Come generare effetti di luce 2D sorprendentemente buoni

Il fulmine ha un sacco di usi nei giochi, dall'ambiente di fondo durante una tempesta ai devastanti attacchi di fulmine di uno stregone. In questo tutorial, spiegherò come generare effetti di luce 2D straordinari: bulloni, rami e persino testo.

Nota: Sebbene questo tutorial sia scritto usando C # e XNA, dovresti essere in grado di utilizzare le stesse tecniche e concetti in quasi tutti gli ambienti di sviluppo di giochi.


Anteprima video finale


Passaggio 1: traccia una linea luminosa

Il componente elementare di base che dobbiamo realizzare è un segmento di linea. Inizia aprendo il tuo software di editing delle immagini preferito e disegnando una linea retta di fulmine. Ecco come appare il mio:

Vogliamo disegnare linee di lunghezze diverse, quindi stiamo andando a tagliare il segmento di linea in tre pezzi come mostrato di seguito. Questo ci permetterà di allungare il segmento centrale a qualsiasi lunghezza che ci piace. Dato che stiamo allungando il segmento centrale, possiamo salvarlo come un solo pixel di spessore. Inoltre, poiché i pezzi sinistro e destro sono immagini speculari l'uno dell'altro, è sufficiente salvarne uno. Possiamo capovolgerlo nel codice.

Ora, dichiariamo una nuova classe per gestire i segmenti della linea di disegno:

linea di classe pubblica public Vector2 A; pubblico Vector2 B; galleggiante pubblico Spessore; public Line ()  public Line (Vector2 a, Vector2 b, float thickness = 1) A = a; B = b; Spessore = spessore; 

A e B sono i punti finali della linea. Ridimensionando e ruotando i pezzi della linea, possiamo disegnare una linea di qualsiasi spessore, lunghezza e orientamento. Aggiungi il seguente Disegnare() metodo per il Linea classe:

public public Draw (SpriteBatch spriteBatch, Color color) Vector2 tangent = B - A; float rotation = (float) Math.Atan2 (tangent.Y, tangent.X); const float ImageThickness = 8; Spessore floatScala = Spessore / 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, null, color, rotation, middleOrigin, middleScale, SpriteEffects.None, 0f); spriteBatch.Draw (Art.HalfCircle, A, null, color, rotation, capOrigin, thicknessScale, SpriteEffects.None, 0f); spriteBatch.Draw (Art.HalfCircle, B, null, color, rotation + MathHelper.Pi, capOrigin, thicknessScale, SpriteEffects.None, 0f); 

Qui, Art.LightningSegment e Art.HalfCircle sono statici Texture2D variabili contenenti le immagini dei pezzi del segmento di linea. ImageThickness è impostato sullo spessore della linea senza il bagliore. Nella mia immagine, è 8 pixel. Impostiamo l'origine del cappuccio sul lato destro e l'origine del segmento centrale sul lato sinistro. Questo li renderà unisci perfettamente quando li disegniamo entrambi nel punto A. Il segmento medio è allungato alla larghezza desiderata, e un altro cappuccio viene disegnato nel punto B, ruotato di 180 °.

XNA di SpriteBatch la classe ti permette di passarlo a SpriteSortMode nel suo costruttore, che indica l'ordine in cui dovrebbe disegnare gli sprite. Quando disegni la linea, assicurati di passarla a SpriteBatch con i suoi SpriteSortMode impostato SpriteSortMode.Texture. Questo per migliorare le prestazioni.

Le schede grafiche sono ottime per disegnare la stessa texture molte volte. Tuttavia, ogni volta che cambiano le trame, c'è un sovraccarico. Se disegniamo un gruppo di linee senza l'ordinamento, disegneremmo le nostre trame in questo ordine:

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

Questo significa che cambieremo textures due volte per ogni linea che disegniamo. SpriteSortMode.Texture dice SpriteBatch per ordinare il Disegnare() chiama dalla trama in modo che tutto il LightningSegments sarà disegnato insieme e tutto il HalfCircles sarà disegnato insieme. Inoltre, quando usiamo queste linee per creare i fulmini, vorremmo usare l'additivo blending per rendere insieme la luce dei pezzi di luce che si sovrappongono.

SpriteBatch.Begin (SpriteSortMode.Texture, BlendState.Additive); // disegna le righe SpriteBatch.End ();

Passaggio 2: linee seghettate

I fulmini tendono a formare linee seghettate, quindi avremo bisogno di un algoritmo per generarli. Lo faremo selezionando punti a caso lungo una linea e spostandoli a una distanza casuale dalla linea. L'uso di uno spostamento completamente casuale tende a rendere la linea troppo frastagliata, quindi attenueremo i risultati limitando quanto lontano dagli altri punti vicini possa essere spostato.

La linea viene lisciata posizionando i punti con un offset simile al punto precedente; questo consente alla linea nel suo complesso di girovagare su e giù, impedendo che una parte di essa sia troppo frastagliata. Ecco il codice:

Elenco statico protetto CreateBolt (origine Vector2, destinazione Vector2, spessore float) risultati var = nuova lista(); Vector2 tangent = dest-source; Vector2 normal = Vector2.Normalize (new Vector2 (tangent.Y, -tangent.X)); float length = tangent.Length (); Elenco posizioni = nuova lista(); positions.Add (0); per (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; float displacement = Rand (-Sway, Sway); dislocamento - = (dislocamento - prev Dislocamento) * (1 - scala); spostamento * = busta; Punto Vector2 = sorgente + pos * tangente + spostamento * normale; results.Add (new Line (prevPoint, point, thickness)); prevPoint = point; prevDisplacement = spostamento;  results.Add (new Line (prevPoint, dest, thickness)); restituire risultati; 

Il codice può sembrare un po 'intimidatorio, ma non è così male una volta che capisci la logica. Iniziamo calcolando i vettori normali e tangenti della linea, insieme alla lunghezza. Quindi selezioniamo casualmente un numero di posizioni lungo la linea e le memorizziamo nella nostra lista di posizioni. Le posizioni sono ridimensionate tra 0 e 1 così 0 rappresenta l'inizio della linea e 1 rappresenta il punto finale. Queste posizioni vengono quindi ordinate per consentirci di aggiungere facilmente segmenti di linea tra di loro.

Il ciclo passa attraverso i punti scelti a caso e li sposta di un importo casuale lungo la normale. Il fattore di scala è lì per evitare angoli eccessivamente nitidi e l'inviluppo garantisce che il fulmine vada effettivamente al punto di destinazione limitando lo spostamento quando siamo vicini alla fine.


Passaggio 3: animazione

I fulmini dovrebbero lampeggiare vivacemente e poi svanire. Per gestire questo, creiamo un Fulmine classe.

class LightningBolt Lista pubblica Segmenti = nuova lista(); float pubblico Alpha get; impostato;  public float FadeOutRate get; impostato;  public Color Tint get; impostato;  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 (origine Vector2, destinazione Vector2, spessore float) // ... // ...

Per usarlo, crea semplicemente un nuovo Fulmine e chiama Aggiornare() e Disegnare() ogni frame. chiamata Aggiornare() lo fa svanire. È completo ti dirà quando il bullone è completamente sbiadito.

Ora puoi disegnare i tuoi bulloni usando il seguente codice nella tua classe di gioco:

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

Passaggio 4: Branch Lightning

Puoi usare il Fulmine classe come elemento di base per creare effetti fulminei più interessanti. Ad esempio, puoi far ruotare i bulloni come mostrato di seguito:

Per creare il ramo del fulmine, selezioniamo punti casuali lungo il fulmine e aggiungiamo nuovi bulloni che si diramano da questi punti. Nel codice seguente, creiamo da tre a sei rami che si separano dal bullone principale a 30 ° di angolo.

class BranchLightning Lista bulloni = nuova lista(); public bool IsComplete get return bolts.Count == 0;  pubblico Vector2 Fine get; set privato;  direzione privata di Vector2; static Random rand = new Random (); public BranchLightning (Vector2 start, Vector2 end) End = end; direction = Vector2.Normalize (end - start); Crea (inizio, fine);  public void Update () bolts = bolts.Where (x =>! x.IsComplete) .ToList (); foreach (var bullone in bulloni) bolt.Update ();  public public Draw (SpriteBatch spriteBatch) foreach (var bolt in bolts) bolt.Draw (spriteBatch);  private void Create (Vector2 start, Vector2 end) var mainBolt = new LightningBolt (inizio, fine); bolts.Add (mainBolt); int numBranches = rand.Next (3, 6); Vector2 diff = end - start; // seleziona un gruppo di punti casuali tra 0 e 1 e li ordina float [] branchPoints = Enumerable.Range (0, numBranches) .Select (x => Rand (0, 1f)) .OrderBy (x => x). ToArray (); per (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;  

Passaggio 5: testo lampo

Di seguito è riportato un video di un altro effetto che puoi ricavare dai fulmini:

Per prima cosa dobbiamo ottenere i pixel nel testo che vorremmo disegnare. Lo facciamo disegnando il nostro testo su a RenderTarget2D e rileggendo i dati dei pixel con RenderTarget2D.GetData(). Se vuoi saperne di più sulla creazione di effetti particellari di testo, qui ho un tutorial più dettagliato.

Memorizziamo le coordinate dei pixel nel testo come a Elenco. Quindi, ogni fotogramma, selezioniamo casualmente coppie di questi punti e creiamo un fulmine tra di loro. Vogliamo progettarlo in modo che più due punti siano vicini l'uno all'altro, maggiore è la possibilità che creiamo un bullone tra di loro. C'è una semplice tecnica che possiamo usare per realizzare questo: sceglieremo il primo punto a caso, e quindi sceglieremo un numero fisso di altri punti a caso e scegliere il più vicino.

Il numero di punti candidati che testeremo influenzerà l'aspetto del testo lampo; il controllo di un numero maggiore di punti ci permetterà di trovare punti molto vicini per disegnare i bulloni tra loro, il che renderà il testo molto pulito e leggibile, ma con un minor numero di fulmini tra le lettere. Numeri più piccoli renderanno il testo dei fulmini più pazzo ma meno leggibile.

public void Update () foreach (var particle in textParticles) float x = particle.X / 500f; if (rand.Next (50) == 0) Vector2 closestParticle = Vector2.Zero; float nearestDist = float.MaxValue; per (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; nearestParticle = altro;  if (nearestDist < 200 * 200 && nearestDist > 10 * 10) bolts.Add (new LightningBolt (particle, nearestParticle, Color.White));  for (int i = bolts.Count - 1; i> = 0; i--) bolts [i] .Update (); if (bolts [i] .Iscompletamento) bolts.RemoveAt (i); 

Passaggio 6: ottimizzazione

Il testo di un fulmine, come mostrato sopra, può essere eseguito senza problemi se si dispone di un computer di fascia alta, ma è sicuramente molto faticoso. Ogni bullone dura oltre 30 fotogrammi e creiamo dozzine di nuovi bulloni per ogni fotogramma. Poiché ogni fulmine può avere fino a un centinaio di segmenti di linea e ogni segmento di linea ha tre pezzi, finiamo per estrarre molti sprite. Il mio demo, ad esempio, disegna oltre 25.000 immagini per ogni fotogramma con le ottimizzazioni disattivate. Possiamo fare di meglio.

Invece di disegnare ogni bullone finché non si attenua, possiamo disegnare ogni nuovo bullone su un bersaglio di rendering e svanire il bersaglio di rendering per ogni fotogramma. Ciò significa che, invece di dover disegnare ogni bullone per 30 o più fotogrammi, lo disegniamo solo una volta. Significa anche che non ci sono costi di prestazioni aggiuntivi per far sparire i nostri fulmini più lentamente e durare più a lungo.

Innanzitutto, modificheremo il LightningText classe per disegnare solo ogni bullone per un fotogramma. Nel tuo Gioco classe, dichiara due RenderTarget2D variabili: currentFrame e lastFrame. Nel LoadContent (), inizializzarli in questo modo:

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

Si noti che il formato della superficie è impostato su HdrBlendable. HDR è l'acronimo di High Dynamic Range e indica che la nostra superficie HDR può rappresentare una gamma di colori più ampia. Questo è necessario perché consente al target di rendering di avere colori più luminosi del bianco. Quando più fulmini si sovrappongono, è necessario che il target di rendering memorizzi la somma completa dei loro colori, che può sommarsi oltre la gamma di colori standard. Mentre questi colori più luminosi del bianco saranno ancora visualizzati come bianchi sullo schermo, è importante memorizzare la loro piena luminosità per farli dissolvere in modo corretto.

Suggerimento XNA: Si noti inoltre che per il funzionamento della tecnica HDR, è necessario impostare il profilo del progetto XNA su Hi-Def. Puoi farlo facendo clic con il tasto destro del mouse sul progetto in Esplora soluzioni, scegliendo le proprietà e quindi scegliendo il profilo hi-def sotto la scheda XNA Game Studio.

Ogni fotogramma, prima estraiamo il contenuto dell'ultimo fotogramma al fotogramma corrente, ma leggermente oscurato. Quindi aggiungiamo tutti i bulloni appena creati al telaio corrente. Infine, rendiamo il nostro frame corrente allo schermo, e quindi scambiamo i due target di rendering in modo che per il nostro prossimo frame, lastFrame si riferirà al frame che abbiamo appena reso.

void DrawLightningText () GraphicsDevice.SetRenderTarget (currentFrame); GraphicsDevice.Clear (Color.Black); // disegna l'ultimo fotogramma al 96% di luminosità spriteBatch.Begin (0, BlendState.Opaque, SamplerState.PointClamp, null, null); spriteBatch.Draw (lastFrame, Vector2.Zero, Color.White * 0.96f); spriteBatch.End (); // disegna nuovi bulloni con additivo blending spriteBatch.Begin (SpriteSortMode.Texture, BlendState.Additive); lightningText.Draw (); spriteBatch.End (); // disegna l'intera cosa al 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; 

Passaggio 7: Altre varianti

Abbiamo discusso di creare testi di fulmini e fulmini, ma certamente non sono gli unici effetti che puoi realizzare. Diamo un'occhiata ad un paio di altre varianti di fulmine che potresti usare.

Moving Lightning

Spesso potresti voler fare un movimento fulmineo. È possibile farlo aggiungendo un nuovo bullone corto a ciascun telaio nel punto finale del bullone del telaio precedente.

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

Smooth Lightning

Potresti aver notato che il lampo brilla più luminoso alle articolazioni. Ciò è dovuto alla miscelazione additiva. Potresti volere uno sguardo più liscio e più uniforme per il tuo fulmine. Questo può essere ottenuto cambiando la funzione di stato di fusione per scegliere il valore massimo dei colori di origine e di destinazione, come mostrato di seguito.

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

Quindi, nel tuo Disegnare() funzione, chiama SpriteBatch.Begin () con maxBlend come il BlendState invece di BlendState.Additive. Le immagini sottostanti mostrano la differenza tra l'additivo blending e il max blending su un fulmine.


Ovviamente la miscelazione massima non consentirà che la luce proveniente da più bulloni o dallo sfondo si sommi bene. Se si desidera che il bullone stesso appaia liscio, ma anche per fondersi in modo additivo con altri bulloni, è possibile prima eseguire il rendering del bullone su un bersaglio di rendering utilizzando il blending massimo e quindi disegnare il target di rendering sullo schermo utilizzando l'additivo blending. Fai attenzione a non utilizzare troppi obiettivi di rendering di grandi dimensioni poiché ciò danneggerebbe le prestazioni.

Un'altra alternativa, che funzionerà meglio per un numero elevato di bulloni, consiste nell'eliminare il bagliore incorporato nelle immagini del segmento di linea e riaggiungerlo utilizzando un effetto bagliore di post-elaborazione. I dettagli sull'uso degli shader e sull'effetto glow sono oltre lo scopo di questo tutorial, ma puoi usare XNA Bloom Sample per iniziare. Questa tecnica non richiederà più obiettivi di rendering man mano che aggiungi altri bulloni.


Conclusione

Il fulmine è un grande effetto speciale per perfezionare i tuoi giochi. Gli effetti descritti in questo tutorial sono un buon punto di partenza, ma non è certamente tutto quello che puoi fare con i fulmini. Con un po 'di immaginazione puoi realizzare ogni tipo di effetti mozzafiato! Scarica il codice sorgente e sperimenta con il tuo.

Se ti è piaciuto questo articolo, dai un'occhiata anche al mio tutorial sugli effetti d'acqua 2D.