Cum de a genera șocant 2D Lightning Efecte

Lightning are o mulțime de utilizări în jocuri, de la mediul înconjurător în timpul unei furtuni până la atacurile fulgerului devastator ale unui vrăjitor. În acest tutorial, vă voi explica cum să generați programabil efecte minunate de 2D fulgere: șuruburi, ramuri și chiar text.

Notă: Deși acest tutorial este scris folosind C # și XNA, ar trebui să puteți utiliza aceleași tehnici și concepte în aproape orice mediu de dezvoltare a jocului.


Vizualizare finală video


Pasul 1: Desenați o linie strălucitoare

Blocul de bază de care trebuie să facem fulgerul este un segment de linie. Începeți prin a vă deschide software-ul preferat de editare a imaginilor și trasând o linie dreaptă de fulgere. Iată ce arată a mea:

Vrem să trasăm linii de lungimi diferite, așa că vom tăia segmentul de linie în trei bucăți, după cum se arată mai jos. Acest lucru ne va permite să întindem segmentul de mijloc la orice lungime ne place. Din moment ce vom întinde segmentul de mijloc, îl putem salva ca pe un singur pixel gros. De asemenea, deoarece piesele din stânga și din dreapta sunt imagini oglindite unele de altele, trebuie doar să salvăm una dintre ele. Îl putem răsturna în cod.

Acum, să declarăm o nouă clasă care să se ocupe de segmentele liniei de desen:

clasa publica publica public Vector2 A; public Vector2 B; float public Grosime; linie publica ()  linie publica (Vector2a, Vector2b, grosime float = 1) A = a; B = b; Grosime = grosime; 

A și B sunt punctele finale ale liniei. Prin scalarea și rotirea bucăților liniei, putem trage o linie de orice grosime, lungime și orientare. Adăugați următoarele A desena() metoda pentru a Linia clasă:

public void Draw (SpriteBatch spriteBatch, culoare) Vector2 tangent = B - A; flotare rotație = (float) Math.Atan2 (tangent.Y, tangent.X); const float ImageThickness = 8; float thicknessScale = grosime / imagineThickness; Vector2 capOrigin = Vector2 nou (Art.HalfCircle.Width, Art.HalfCircle.Height / 2f); Vector2 middleOrigin = Vector2 nou (0, Art.LightningSegment.Height / 2f); Vector2 middleScale = Vector2 nou (tangent.Length (), grosimeScale); spriteBatch.Draw (Art.LightningSegment, A, null, culoare, rotire, middleOrigin, middleScale, SpriteEffects.None, 0f); spriteBatch.Draw (Art.HalfCircle, A, null, culoare, rotire, capOrigin, thicknessScale, SpriteEffects.None, 0f); spriteBatch.Draw (Art.HalfCircle, B, null, culoare, rotire + MathHelper.Pi, capOrigin, thicknessScale, SpriteEffects.None, 0f); 

Aici, Art.LightningSegment și Art.HalfCircle sunt statice Texture2D variabile care dețin imaginile pieselor segmentului de linie. ImageThickness este setat la grosimea liniei fără strălucire. În imaginea mea, sunt 8 pixeli. Am stabilit originea capacului în partea dreaptă și originea segmentului de mijloc în partea stângă. Acest lucru le va face să se alăture fără probleme atunci când le atragem amândouă în punctul A. Segmentul mijlociu este întins la lățimea dorită și un alt capac este desenat la punctul B, rotit la 180 °.

lui XNA SpriteBatch clasa vă permite să o treceți SpriteSortMode în constructorul său, care indică ordinea în care ar trebui să deseneze spritele. Când trageți linia, asigurați-vă că o treceți SpriteBatch cu al ei; cu al lui SpriteSortMode setat la SpriteSortMode.Texture. Acest lucru este de a îmbunătăți performanța.

Cărțile grafice sunt excelente pentru desenarea aceleiași texturi de mai multe ori. Cu toate acestea, de fiecare dată când schimba texturile, există overhead. Dacă desenați o grămadă de linii fără sortare, ne-am desena texturile noastre în această ordine:

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

Aceasta înseamnă că vom schimba texturile de două ori pentru fiecare linie pe care o tragem. SpriteSortMode.Texture spune SpriteBatch pentru a sorta A desena() apeluri prin textura, astfel încât toate LightningSegments vor fi trase împreună și toate HalfCircles vor fi desenate împreună. În plus, atunci când folosim aceste linii pentru a face fulgere, am dori să folosim amestecarea aditivilor pentru a face ca lumina să se adune împreună cu bucățile de fulgere care se suprapun.

SpriteBatch.Begin (SpriteSortMode.Texture, BlendState.Additiv); // trasați liniile SpriteBatch.End ();

Pasul 2: Linii jigged

Fulgerul are tendința de a forma linii zimțate, deci avem nevoie de un algoritm pentru a le genera. Vom face acest lucru prin alegerea de puncte la întâmplare de-a lungul unei linii și deplasarea acestora la o distanță aleatorie de linie. Folosind o deplasare complet aleatorie tinde să facă linia prea îndoită, așa că vom lămuri rezultatele prin limitarea cât de departe pot fi deplasate punctele învecinate.

Linia este netezită prin plasarea punctelor la o deplasare similară punctului anterior; acest lucru permite ca linia ca întreg să se rătăcească în sus și în jos, în timp ce împiedică orice parte din ea să fie prea zgârcită. Iată codul:

listă statică protejată CreateBolt (sursa Vector2, Vector2 dest, float grosime) var results = new List(); Vector2 tangent = dest - sursă; Vector2 normal = Vector2.Normalize (Vector2 nou (tangent.Y, -tangent.X)); lungimea flotă = tangentă. Lungime (); Listă positions = listă nouă(); positions.Add (0); pentru (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; deplasare în flotă = Rand (-Distă, Sway); deplasare - = (deplasare - previziune) * (1 - scală); deplasare * = plic; Vector2 punct = sursă + pos * tangent + deplasare * normal; results.Add (linie nouă (prevPoint, punct, grosime)); prevPoint = punct; prevDisplacement = deplasare;  results.Add (noua linie (prevPoint, dest, grosime)); rezultatele returnate; 

Codul poate părea un pic intimidant, dar nu este așa de rău când înțelegeți logica. Începem prin calcularea vectorilor normali și tangenți ai liniei, împreună cu lungimea. Apoi alegem aleator un număr de poziții de-a lungul liniei și le stocăm în lista noastră de poziții. Pozițiile sunt scalate între 0 și 1 astfel încât 0 reprezintă începutul liniei și 1 reprezintă punctul final. Aceste poziții sunt apoi sortate pentru a ne permite să adăugăm cu ușurință segmente de linie între ele.

Buclele trece prin punctele alese aleatoriu și le deplasează de-a lungul normalelor cu o valoare aleatorie. Factorul de scalare este acolo pentru a evita unghiurile prea ascuțite, iar plicul asigură faptul că fulgerul ajunge la punctul de destinație prin limitarea deplasării când suntem aproape de final.


Pasul 3: Animație

Fulger ar trebui să clipească luminos și apoi se estompeze. Pentru a rezolva acest lucru, să creăm a Fulger clasă.

clasa LightningBolt public List Segmente = Listă nouă(); public float Alpha a lua; a stabilit;  float public FadeOutRate get; a stabilit;  public Tint color get; a stabilit;  boolean public 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 (sursa Vector2, Vector2 dest, float thickness) // ... // ...

Pentru a utiliza acest lucru, creați pur și simplu un nou Fulger și sunați Actualizați() și A desena() fiecare cadru. apel Actualizați() face să se estompeze. Este complet vă va spune când bolțul a dispărut complet.

Acum puteți trage șuruburile folosind codul următor în clasa de joc:

Fulger; MouseState mouse-ul, lastMouseState; suprascriere protejată void Actualizare (GameTime gameTime) lastMouseState = mouseState; mouseState = Mouse.GetState (); var screenSize = nou Vector2 (GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height); var mousePosition = nou Vector2 (mouseState.X, mouseState.Y); dacă (MouseWasClicked ()) Bolt = noul LightningBolt (screenSize / 2, mousePosition); dacă (șurub! = nulă) bolt.Update ();  bool privat MouseWasClicked () return mouseState.LeftButton == ButtonState.Pressed && lastMouseState.LeftButton == ButtonState.Released;  suprascriere protejată void Draw (GameTime gameTime) GraphicsDevice.Clear (Color.Black); spriteBatch.Begin (SpriteSortMode.Texture, BlendState.Additiv); dacă (șurub! = nulă) bolt.Draw (spriteBatch); spriteBatch.End (); 

Pasul 4: Lightning Branch

Puteți utiliza funcția Fulger clasa ca un bloc de construcție pentru a crea mai interesante efecte de fulger. De exemplu, puteți face șuruburile să se extindă după cum se arată mai jos:

Pentru a face ramura fulgerului, alegem puncte aleatorii de-a lungul fulgerului și adăugați noi șuruburi care se extind din aceste puncte. În codul de mai jos, vom crea între trei și șase ramuri care se separă de șurubul principal la unghiuri de 30 °.

clasa BranchLightning Listă bolțuri = listă nouă(); booleanul public IsComplete get return bolts.Count = = 0;  Vector2 End public get; set privat;  direcție Vector2 privată; static Rand rand = nou Random (); public BranchLightning (start Vector2, sfârșitul Vector2) End = end; direcția = Vector2.Normalize (end-start); Creați (început, sfârșit);  public void Actualizare () bolți = bolți.În cazul în care (x =>! x.IsComplete) .ToList (); foreach (var bolt în șuruburi) bolt.Update ();  void public Draw (SpriteBatch spriteBatch) foreach (var bolt în bolțuri) bolt.Draw (spriteBatch);  void privat Creare (Vector2 start, Vector2 end) var mainBolt = nou LightningBolt (start, end); bolts.Add (mainBolt); int numBranches = rand.Next (3, 6); Vector2 diff = end - start; // alege o gramada de puncte aleatorii intre 0 si 1 si sorteaza-le float [] branchPoints = Enumerable.Range (0, numBranches) .Selecteaza (x => Rand (0, 1f)) .OrderBy (x => x). ToArray (); pentru (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;  

Pasul 5: textul cu fulgere

Mai jos este un videoclip cu un alt efect pe care îl puteți face din fulgerul:

Mai intai trebuie sa obtinem pixelii in textul pe care dorim sa-l deseneaza. Facem acest lucru prin redactarea textului nostru la un RenderTarget2D și citirea datelor cu ajutorul pixelilor RenderTarget2D.GetData(). Dacă doriți să citiți mai multe despre efectul de particule de text, am un tutorial mai detaliat aici.

Stocam coordonatele pixelilor in text ca a Listă. Apoi, în fiecare cadru, alegem aleatoriu perechi de puncte și creăm un fulger între ele. Vrem să o proiectăm astfel încât cele două puncte mai strânse să fie una cu cealaltă, cu atât mai mare este șansa de a crea un șurub între ele. Există o tehnică simplă pe care o putem folosi pentru a realiza acest lucru: vom alege primul punct la întâmplare și apoi vom alege un număr fix de alte puncte la întâmplare și vom alege cel mai apropiat punct.

Numărul de puncte candidate pe care le testăm va afecta aspectul textului de fulger; verificarea unui număr mai mare de puncte ne va permite să găsim puncte foarte apropiate pentru a trage șuruburile între ele, ceea ce va face textul foarte curat și lizibil, dar cu mai puține fulgere lungi între litere. Numerele mai mici vor face textul fulger să arate mai nebun, dar mai puțin lizibil.

public void Actualizare () foreach (particulă var în textParticles) float x = particle.X / 500f; dacă (rand.Next (50) == 0) Vector2 nearestParticle = Vector2.Zero; float nearestDist = float.MaxValue; pentru (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; cel mai apropiatParticule = altul;  dacă (cel mai apropiatDist < 200 * 200 && nearestDist > 10 * 10) bolți.Adăugați (noul LightningBolt (particulă, cel mai apropiat Particulat, Culoarea Albă));  pentru (int i = șuruburi.Count - 1; i> = 0; i -) șuruburi [i] .Update (); dacă (șuruburi [i] .Iscomplete) bolts.RemoveAt (i); 

Pasul 6: Optimizare

Textul de fulger, așa cum este arătat mai sus, poate funcționa fără probleme dacă aveți un computer de linie, dar cu siguranță este foarte impozabil. Fiecare bolț durează peste 30 de cadre și noi creăm zeci de șuruburi noi în fiecare cadru. Deoarece fiecare fulger poate avea până la câteva sute de segmente de linie, iar fiecare segment de linie are trei bucăți, ajungem să desenăm o mulțime de sprite. Demo-ul meu, de exemplu, desenează peste 25.000 de imagini în fiecare cadru, optimizările fiind dezactivate. Putem face mai bine.

În loc să tragem fiecare șurub până când acesta se estompează, putem trage fiecare șurub nou într-o țintă de randare și putem distruge țintă pentru fiecare cadru. Acest lucru înseamnă că, în loc să fie nevoie să tragem fiecare șurub pentru 30 sau mai multe cadre, tragem o singură dată. Aceasta înseamnă, de asemenea, că nu există costuri suplimentare de performanță pentru ca fulgerul să se estompeze mai încet și să dureze mai mult.

În primul rând, vom modifica LightningText clasa de a trage doar fiecare șurub pentru un cadru. În tine Joc clasa, declarați două RenderTarget2D variabile: currentFrame și lastFrame. În LoadContent (), inițializați-le astfel:

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

Observați că formatul suprafeței este setat la HdrBlendable. HDR înseamnă o gamă de înaltă dinamică și indică faptul că suprafața HDR poate reprezenta o gamă mai largă de culori. Acest lucru este necesar deoarece permite ca țintă de randare să aibă culori mai strălucitoare decât albul. Când se suprapun mai multe surse de lumină, avem nevoie de ținta de redare pentru a stoca suma completă a culorilor, care se pot adăuga dincolo de gama standard de culori. În timp ce aceste culori mai strălucitoare decât cele albe vor fi încă afișate ca alb pe ecran, este important să le stocați luminozitatea completă pentru a le face să se estompeze corect.

Sfat XNA: De asemenea, rețineți că pentru amestecarea HDR pentru a funcționa, trebuie să setați profilul proiectului XNA la Hi-Def. Puteți face acest lucru făcând clic dreapta pe proiect în exploratorul de soluții, selectând proprietățile și apoi selectând profilul hi-def din fila XNA Game Studio.

Fiecare cadru, tragem mai întâi conținutul ultimului cadru pe cadrul curent, dar ușor întunecat. Apoi adăugăm orice șurub nou creat în cadrul actual. În cele din urmă, renderăm cadrul nostru actual pe ecran și apoi schimbăm cele două direcții de direcționare, astfel încât pentru următorul cadru, lastFrame se va referi la cadrul pe care tocmai l-am redat.

void DrawLightningText () GraphicsDevice.SetRenderTarget (actualFrame); GraphicsDevice.Clear (Color.Black); // trage ultimul cadru la luminozitatea de 96% spriteBatch.Begin (0, BlendState.Opaque, SamplerState.PointClamp, null, null); spriteBatch.Draw (lastFrame, Vector2.Zero, Color.White * 0.96f); spriteBatch.End (); // trageți noi șuruburi cu amestec de aditivi spriteBatch.Begin (SpriteSortMode.Texture, BlendState.Additive); lightningText.Draw (); spriteBatch.End (); // trageți totul la backbufferul GraphicsDevice.SetRenderTarget (null); spriteBatch.Begin (0, BlendState.Opaque, SamplerState.PointClamp, null, null); spriteBatch.Draw (actualFrame, Vector2.Zero, Color.White); spriteBatch.End (); Schimbați (ref curentFrame, ref lastFrame);  void Swap(ref T a, ref T b) T temp = a; a = b; b = temp; 

Pasul 7: Alte variații

Am discutat despre realizarea fulgerului în ramură și a textului cu fulgere, dar cu siguranță nu sunt singurele efecte pe care le puteți face. Să ne uităm la câteva alte variante ale fulgerului pe care le puteți utiliza.

Mutarea fulgerului

Adesea, poate doriți să faceți un fulger în mișcare. Puteți face acest lucru prin adăugarea unui nou șurub scurt la fiecare cadru la punctul final al șurubului anterior al cadrului.

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

Smooth Lightning

S-ar putea să fi observat că fulgerul strălucește mai strălucitor la articulații. Acest lucru se datorează amestecului de aditivi. S-ar putea să vă doriți o lumină mai liniștită și mai uniformă. Acest lucru poate fi realizat prin schimbarea funcției dvs. de amestecare pentru a alege valoarea maximă a culorilor sursă și de destinație, după cum se arată mai jos.

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

Apoi, în tine A desena() funcție, apel SpriteBatch.Begin () cu maxBlend dupa cum BlendState in loc de BlendState.Additive. Imaginile de mai jos prezintă diferența dintre amestecarea aditivilor și amestecarea maximă pe un fulger.


Desigur, amestecarea maximă nu va permite luminii de la mai multe șuruburi sau din fundal să se adune frumos. Dacă doriți ca șurubul să pară neted, dar și să se amestece în mod adițional cu alte șuruburi, puteți face mai întâi șurubul la un obiect de randare utilizând amestecarea maximă și apoi trageți țintă pentru render pe ecran utilizând amestecarea aditivilor. Aveți grijă să nu folosiți prea multe ținte mari de randare, deoarece acest lucru va afecta performanța.

O altă alternativă, care va funcționa mai bine pentru un număr mare de șuruburi, este de a elimina strălucirea încorporată în imaginile segmentului de linie și de ao adăuga înapoi utilizând un efect de strălucire ulterioară. Detaliile despre utilizarea shaderelor și de a face efecte strălucitoare sunt dincolo de scopul acestui tutorial, dar puteți utiliza modelul XNA Bloom Sample pentru a începe. Această tehnică nu va necesita mai multe obiective de randare pe măsură ce adăugați mai multe șuruburi.


Concluzie

Fulgerul este un efect deosebit deosebit pentru a vă îmbogăți jocurile. Efectele descrise în acest tutorial sunt un punct de plecare frumos, dar cu siguranță nu este tot ce poți face cu fulgere. Cu un pic de imaginație puteți face tot felul de efecte de fulger de inspirație uimitoare! Descărcați codul sursă și experimentați-vă cu propriul dvs..

Dacă vă place acest articol, aruncați o privire la tutorialul meu despre efecte de apă 2D, de asemenea.