Faceți un shooter vector neon în XNA Bloom and Holes Black

În această serie de tutoriale, vă voi arăta cum să faceți un împușcătură cu stick-uri neon, cum ar fi Geometry Wars, în XNA. Scopul acestor tutoriale nu este să vă lase o replică exactă a războaielor Geometry, ci mai degrabă să treceți peste elementele necesare care vă vor permite să creați propria varianta de înaltă calitate.


Prezentare generală

În seria de până acum, am stabilit gameplay-ul de bază pentru shooter-ul nostru de gemeni, Shape Blaster. În acest tutorial vom crea aspectul neon semnătură prin adăugarea unui filtru de post-procesare înflorit.

Avertizare: Tare!

Efectele simple, cum ar fi acest lucru sau efectele particulelor, pot face un joc mult mai atrăgător, fără a necesita modificări în modul de joc. Utilizarea efectivă a efectelor vizuale este un aspect important în orice joc. După adăugarea filtrului înflorit, vom adăuga și găuri negre la joc.


Efectul post-procesare înflorit

Bloom descrie efectul pe care îl vezi când te uiți la un obiect cu o lumină puternică în spatele lui și lumina pare să sângereze peste obiect. În Shape Blaster, efectul de înflorire va face ca liniile strălucitoare ale navelor și particulelor să arate ca lumini strălucitoare, stralucitoare, de neon.

Lumina soarelui înflorită prin copaci

Pentru a aplica înflorire în jocul nostru, trebuie să redăm scena noastră la o țintă de redare și apoi să aplicăm filtrul nostru de inflorescență la acea țintă de redare.

Bloom funcționează în trei etape:

  1. Extrageți părțile luminoase ale imaginii.
  2. Blur părțile luminoase.
  3. Recombinați imaginea neclară cu imaginea originală în timp ce efectuați anumite reglaje de luminozitate și saturație.

Fiecare dintre acești pași necesită a Shader - în esență, un program scurt care rulează pe placa grafică. Shaders în XNA sunt scrise într-o limbă specială numită Limbă Shader de nivel înalt (HLSL). Imaginile de mai jos arată rezultatul fiecărui pas.

Imaginea inițială Zonele luminoase extrase din imagine Zonele luminoase după estompare Rezultatul final după recombinarea cu imaginea originală

Adăugarea Bloom la Blaster Shape

Pentru filtrul nostru de floare, vom folosi modelul XNA Bloom Postprocess.

Integrarea probelor de flori cu proiectul nostru este ușoară. Mai întâi, localizați cele două fișiere de cod din eșantion, BloomComponent.cs și BloomSettings.cs, și adăugați-le la ShapeBlaster proiect. Adăugați, de asemenea BloomCombine.fx, BloomExtract.fx, și GaussianBlur.fx la proiectul de conducte de conținut.

În GameRoot, adauga o utilizând declarație pentru BloomPostprocess spațiu de nume și adăugați o BloomComponent variabila de membru.

 BloomComponent floare;

În GameRoot constructor, adăugați următoarele linii.

 bloom = nou BloomComponent (acest lucru); Components.Add (floare); bloom.Settings = setări noi BloomSettings (null, 0.25f, 4, 2, 1, 1.5f, 1);

În cele din urmă, chiar la începutul anului GameRoot.Draw (), adăugați următoarea linie.

 bloom.BeginDraw ();

Asta e. Dacă executați jocul acum, ar trebui să vedeți înflorirea în vigoare.

Când sunați bloom.BeginDraw (), redirecționează apelurile ulterioare către o destinație de randare la care va fi aplicată floarea. Când sunați base.Draw () la sfârșitul GameRoot.Draw () metodă, BloomComponent„s A desena() se numește metoda. Aici este aplicată floarea și scena este trasă spre tamponul din spate. Prin urmare, trebuie să se traseze tot ce trebuie aplicat în floare între apelurile către bloom.BeginDraw () și base.Draw ().

Bacsis: Dacă doriți să desenați ceva fără floare (de exemplu, interfața cu utilizatorul), trageți-o după apelul către base.Draw ().

Aveți posibilitatea să tweak setările de floare la preferințele dumneavoastră. Am ales următoarele valori:

  • 0,25 pentru pragul de inflorire. Aceasta înseamnă că orice părți ale imaginii care sunt mai puțin de un sfert din luminozitatea completă nu vor contribui la înflorire.
  • 4 pentru suma neclară. Pentru înclinația matematică, aceasta este abaterea standard a neclarității Gaussian. Valori mai mari vor bloca lumina mai mult. Cu toate acestea, rețineți că umbra de estompare este setată să utilizeze un număr fix de eșantioane, indiferent de valoarea neclarității. Dacă setați această valoare prea mare, estomparea se va extinde dincolo de raza de la care vor apărea mostrele de shader și artefactele. În mod ideal, această valoare nu ar trebui să fie mai mare de o treime din raza de eșantionare pentru a vă asigura că eroarea este neglijabilă.
  • 2 pentru intensitatea inflorescenței, care determină cât de puternic afectează floarea rezultatul final.
  • 1 pentru intensitatea bazei, care determină cât de puternic influențează imaginea originală rezultatul final.
  • 1.5 pentru saturația înflorire. Acest lucru face ca strălucirea obiectelor strălucitoare să aibă mai multe culori saturate decât obiectele în sine. O valoare mare a fost aleasă pentru a simula aspectul luminilor de neon. Dacă vă uitați la centrul unei lumini neon luminoase, aceasta arată aproape albă, în timp ce strălucirea din jurul ei este mai puternic colorată.
  • 1 pentru saturația de bază. Această valoare afectează saturația imaginii de bază.
Fără floare Cu floare

Bloom Sub Hood

Filtrul înflorit este implementat în BloomComponent clasă. Componenta de inflorire incepe prin crearea si incarcarea resurselor necesare LoadContent () metodă. Aici, se încarcă cele trei shadere pe care le necesită și creează trei obiective de randare.

Prima țintă de redare, sceneRenderTarget, este pentru a ține scena pe care va fi aplicată floarea. Celelalte doua, renderTarget1 și renderTarget2, sunt utilizate pentru a menține temporar rezultatele intermediare între fiecare permis de redare. Aceste obiective de redare se fac la jumătate din rezoluția jocului pentru a reduce costul de performanță. Acest lucru nu reduce calitatea finală a floarelor, pentru că, oricum, vom bloca imaginile înflorite.

Bloom necesită patru treceri de redare, după cum se arată în această diagramă:

În XNA, Efect clasa încapsulează un shader. Scrieți codul pentru shader în fișier separat, pe care îl adăugați la conducta de conținut. Acestea sunt fișierele cu .fx pe care am adăugat-o mai devreme. Încărcați shaderul într-un Efect obiect apelând Content.Load() metoda în LoadContent (). Cea mai ușoară modalitate de a utiliza un shader într-un joc 2D este să treceți Efect obiect ca parametru pentru SpriteBatch.Begin ().

Există mai multe tipuri de shadere, dar pentru filtrul de bloom pe care îl vom folosi numai pixeli de shadere (uneori numite fragmentare shadere). Un shader pixel este un program mic care rulează o dată pentru fiecare pixel pe care îl desenați și determină culoarea pixelului. Vom trece peste fiecare dintre shaderele folosite.

BloomExtract Shader

BloomExtract shader este cel mai simplu dintre cele trei shadere. Obiectivul său este de a extrage zonele imaginii care sunt mai strălucitoare decât un anumit prag și apoi să rescaleze valorile culorilor pentru a utiliza întreaga gamă de culori. Orice valoare sub prag va deveni negru.

Codul shader complet este prezentat mai jos.

 sampler TextureSampler: registru (s0); float BloomThreshold; float4 PixelShaderFunction (float2 texCoord: TEXCOORD0): COLOR0 // Căutați culoarea originală a imaginii. float4 c = tex2D (TextureSampler, texCoord); // Reglați-l pentru a păstra numai valorile mai luminoase decât pragul specificat. returnați saturați ((c - BloomThreshold) / (1 - BloomThreshold));  tehnica BloomExtract pass Pass1 PixelShader = compilați ps_2_0 PixelShaderFunction (); 

Nu vă faceți griji dacă nu sunteți familiarizat cu HLSL. Să examinăm cum funcționează aceasta.

 sampler TextureSampler: registru (s0);

Această primă parte declară un sampler de textură numit TextureSampler. SpriteBatch va lega o textura la acest sampler atunci cand trage cu acest shader. Specificarea registrului pentru care se leagă este opțional. Folosim sampler-ul pentru a căuta pixeli din textura legată.

 float BloomThreshold;

BloomThreshold este un parametru pe care îl putem seta din codul nostru C #.

 float4 PixelShaderFunction (float2 texCoord: TEXCOORD0): COLOR0 

Aceasta este declarația funcției shader pixel care ia coordonatele de textura ca intrare și returnează o culoare. Culoarea este returnată ca a float4. Aceasta este o colecție de patru flotoare, la fel ca a Vector4 în XNA. Ele stochează componentele roșu, verde, albastru și alfa ale culorii ca valori între zero și una.

TEXCOORD0 și COLOR0 sunt numite semantică, și le indică compilatorului modul în care texCoord parametrul și valoarea de retur sunt utilizate. Pentru fiecare ieșire de pixeli, texCoord va conține coordonatele punctului corespunzător din textura de intrare, cu (0, 0) fiind colțul din stânga sus și (1, 1) fiind în partea dreaptă jos.

 // Căutați culoarea originală a imaginii. float4 c = tex2D (TextureSampler, texCoord); // Reglați-l pentru a păstra numai valorile mai luminoase decât pragul specificat. returnați saturați ((c - BloomThreshold) / (1 - BloomThreshold));

Aici se face toată munca reală. Se preia culoarea pixelului din textură BloomThreshold din fiecare componentă de culoare și apoi o scalați înapoi, astfel încât valoarea maximă să fie una. satura() funcția apoi fixează componentele culorii între zero și una.

S-ar putea să observați asta c și BloomThreshold nu sunt de același tip c este a float4 și BloomThreshold este a pluti. HLSL vă permite să efectuați operațiuni cu aceste tipuri diferite, rotind în esență pluti intr-o float4 cu toate componentele la fel. (c - BloomThreshold) devine:

 c - float4 (BloomThreshold, BloomThreshold, BloomThreshold, BloomThreshold)

Restul shader-ului creează pur și simplu o tehnică care utilizează funcția shader pixel, compilate pentru modelul shader 2.0.

GaussianBlur Shader

O estompare Gaussian estompează o imagine folosind o funcție Gaussiană. Pentru fiecare pixel din imaginea de ieșire, rezumăm pixelii din imaginea de intrare ponderată de distanța lor față de pixelul țintă. Pixelii din apropiere contribuie foarte mult la culoarea finală, în timp ce pixelii îndepărtați contribuie foarte puțin.

Deoarece pixelii îndepărtați fac contribuții neglijabile și deoarece căutările de textură sunt costisitoare, vom eșantiona doar pixeli într-o rază scurtă în loc să prelevăm întreaga textură. Acest shader va preleva puncte în limitele a 14 pixeli din pixelul curent.

O implementare naivă ar putea să probeze toate punctele dintr-un pătrat în jurul pixelului curent. Cu toate acestea, acest lucru poate fi costisitor. În exemplul nostru, ar trebui să probeze puncte într-un pătrat de 29x29 (14 puncte pe ambele părți ale pixelului central, plus pixelul central). Acesta este un total de 841 de probe pentru fiecare pixel în imaginea noastră. Din fericire, există o metodă mai rapidă. Se pare că realizarea unei estompare Gaussiană 2D este echivalentă cu estomparea inițială a imaginii pe orizontală și apoi estomparea acesteia din nou vertical. Fiecare dintre aceste blururi unidimensionale necesită doar 29 de eșantioane, reducând totalul nostru la 58 de probe pe pixel.

Încă un truc este folosit pentru a crește în continuare eficiența neclarității. Când îi spui GPU-ului să probeze între doi pixeli, acesta va reveni la un amestec al celor doi pixeli, fără costuri suplimentare de performanță. Deoarece neclaritatea noastră amestecă pixeli împreună, acest lucru ne permite să încercăm doi pixeli la un moment dat. Acest lucru reduce numărul de probe necesare în aproape jumătate.

Mai jos sunt elementele relevante ale GaussianBlur Shader.

 sampler TextureSampler: registru (s0); #define SAMPLE_COUNT 15 float2 SampleOffsets [SAMPLE_COUNT]; float SampleWeights [SAMPLE_COUNT]; float4 PixelShaderFunction (float2 texCoord: TEXCOORD0): COLOR0 float4 c = 0; Combinați un număr de robinete de filtrare a imaginii ponderate. pentru (int i = 0; i < SAMPLE_COUNT; i++)  c += tex2D(TextureSampler, texCoord + SampleOffsets[i]) * SampleWeights[i];  return c; 

Shader-ul este de fapt destul de simplu; aceasta ia doar o serie de compensări și o gamă corespunzătoare de greutăți și calculează suma ponderată. Tot matematica complexă este de fapt în codul C # care găzduiește matricele de offset și greutate. Acest lucru se face în SetBlurEffectParameters () și ComputeGaussian () metode ale BloomComponent clasă. Când efectuați trecerea orară neclară, SampleOffsets vor fi populate numai cu compensări orizontale (componentele y sunt toate zero) și, bineînțeles, inversarea este adevărată pentru pasul vertical.

BloomCombine Shader

BloomCombine shader face câteva lucruri deodată. Combină textura înflorită cu textura originală, ajustând în același timp intensitatea și saturația fiecărei texturi.

Shader-ul începe prin declararea a două samplere de textură și patru parametri float.

 sampler BloomSampler: registru (s0); sampler BaseSampler: registru (s1); float BloomIntensity; float BaseIntensity; Float BloomSaturation; float BaseSaturation;

Un lucru trebuie menționat SpriteBatch va lega automat textura pe care o transmiteți atunci când apelați SpriteBatch.Draw () la primul eșantionator, dar nu va lega automat nimic la cel de-al doilea eșantionator. Al doilea eșantionator este setat manual în BloomComponent.Draw () cu următoarea linie.

 GraphicsDevice.Texturi [1] = sceneRenderTarget;

Apoi avem o funcție de ajutor care ajustează saturația unei culori.

 float4 AdjustSaturation (culoare float4, saturație flotantă) // Constantele 0.3, 0.59 și 0.11 sunt alese deoarece ochiul uman este mai sensibil la lumina verde și mai puțin la albastru. float gri = punct (culoare, float3 (0,3, 0,59, 0,11)); retur lerp (gri, culoare, saturație); 

Această funcție are o valoare de culoare și o valoare de saturație și returnează o nouă culoare. Trecerea unei saturații a 1 lasă culoarea neschimbată. Trecere 0 va reveni în gri și valorile trecute mai mari decât una vor reveni la o culoare cu o saturație crescută. Trecerea valorilor negative este într-adevăr în afara utilizării intenționate, dar va inversa culoarea dacă faceți acest lucru.

Funcția funcționează mai întâi prin găsirea luminozității culorii prin luarea unei sume ponderate bazată pe sensibilitatea ochilor noștri la lumina roșie, verde și albastră. Apoi interpolează liniar între gri și culoarea originală cu cantitatea de saturație specificată. Această funcție este apelată de funcția shader pixel.

 float4 PixelShaderFunction (float2 texCoord: TEXCOORD0): COLOR0 // Căutați culoarea originală și culoarea originală. float4 bloom = tex2D (BloomSampler, texCoord); float4 bază = tex2D (BaseSampler, texCoord); // Ajustați saturația și intensitatea culorii. bloom = AjustareSaturatie (Bloom, BloomSaturation) * BloomIntensity; base = AdjustSaturation (bază, BaseSaturation) * BaseIntensity; // Întunecați imaginea de bază în zone în care există o mulțime de flori, // pentru a împiedica lucrurile care arata excesiv de arse. baza * = (1 - saturate (bloom)); // Combinați cele două imagini. baza de întoarcere + floare; 

Din nou, acest shader este destul de simplu. Dacă vă întrebați de ce imaginea de bază trebuie să fie întunecată în zonele cu flori strălucitoare, rețineți că adăugarea a două culori împreună crește luminozitatea și orice componente de culoare care adună o valoare mai mare decât una (luminozitatea completă) vor fi tăiate într-o singură . Deoarece imaginea de înflorire este similară imaginii de bază, aceasta ar face ca o mare parte a imaginii care are o luminozitate de peste 50% să devină maximă. Întunecarea imaginii de bază duce la redarea tuturor culorilor în gama de culori pe care le putem afișa în mod corespunzător.


Găuri negre

Unul dintre cei mai interesați dușmani din Geometry Wars este gaura neagră. Să examinăm cum putem face ceva similar în Shape Blaster. Vom crea funcționalitatea de bază acum și vom revizui inamicul în tutorialul următor pentru a adăuga efectele particulelor și interacțiunile particulelor.

O gaură neagră cu particule care orbitează

Funcționalitatea de bază

Găurile negre vor trage în nava jucătorului, inamicii din apropiere și (după următorul tutorial) particule, dar vor respinge gloanțele.

Există multe funcții posibile pe care le putem folosi pentru atracție sau repulsie. Cel mai simplu este să folosiți forța constantă astfel încât gaura neagră să tragă cu aceeași forță indiferent de distanța obiectului. O altă opțiune este ca forța să crească liniar de la zero la o anumită distanță maximă, până la rezistență maximă pentru obiectele direct deasupra găurii negre.

Dacă am dori să modelăm gravitatea mai realist, putem folosi pătratul invers al distanței, ceea ce înseamnă că forța gravitației este proporțională cu \ (1 / distanța ^ 2). De fapt, vom folosi fiecare dintre aceste trei funcții pentru a gestiona diferite obiecte. Gloanțele vor fi respinse cu forță constantă, dușmanii și nava jucătorului vor fi atrase cu o forță liniară, iar particulele vor folosi o funcție pătrată inversă.

Vom face o nouă clasă pentru găurile negre. Să începem cu funcționalitatea de bază.

 clasa BlackHole: Entitatea private static Random rand = new Random (); private hitpoints int = 10; public BlackHole (Poziția Vector2) image = Art.BlackHole; Poziție = poziție; Radius = imagine.Width / 2f;  void publice WasShot () hitpoints--; dacă (puncte hit <= 0) IsExpired = true;  public void Kill()  hitpoints = 0; WasShot();  public override void Draw(SpriteBatch spriteBatch)  // make the size of the black hole pulsate float scale = 1 + 0.1f * (float)Math.Sin(10 * GameRoot.GameTime.TotalGameTime.TotalSeconds); spriteBatch.Draw(image, Position, null, color, Orientation, Size / 2f, scale, 0, 0);  

Găurile negre au zece fotografii pentru a ucide. Reglează ușor scala spritei pentru a face să se pulseze. Dacă decideți că distrugerea găurilor negre ar trebui să acorde și puncte, trebuie să efectuați ajustări similare la Gaură neagră așa cum am făcut-o cu clasa inamicului.

Apoi vom face ca găurile negre să aplice efectiv o forță asupra altor entități. Vom avea nevoie de o mică metodă de ajutor de la noi EntityManager.

 public static IEnumerable GetNearbyEntities (poziția Vector2, raza float) return entities.Where (x => Vector2.DistanceSquared (position, x.Position) < radius * radius); 

Această metodă ar putea deveni mai eficientă prin utilizarea unei scheme de partiționare spațială mai complicată, dar pentru numărul de entități pe care le vom avea, este bine așa cum este. Acum putem face ca găurile negre să aplice forța lor Actualizați() metodă.

 suprascriere publică void Actualizare () var entities = EntityManager.GetNearbyEntities (Poziție, 250); fore (entitatea var în entități) if (entitatea este Enemy &&! (entitate ca Enemy) .IsActive) continuă; // gloante sunt respinse prin găuri negre și orice altceva este atras dacă (entitatea este Bullet) entitate.Velocitate + = (entitate.Poziție - Poziție) .ScaleTo (0.3f); altfel var dPos = Poziție - entitate.Poziție; var lungime = dPos.Length (); entity.Velocity + = dPos.ScaleTo (MathHelper.Lerp (2, 0, lungime / 250f)); 

Găurile negre afectează numai entitățile dintr-o rază aleasă (250 pixeli). Gloanțele din această rază au o forță constantă respingătoare aplicată, în timp ce orice altceva are o forță liniară atrăgătoare aplicată.

Va trebui să adăugăm o manipulare a coliziunilor pentru găurile negre la EntityManager. Adauga o Listă <> pentru găurile negre ca și în cazul celorlalte tipuri de entități și adăugați următorul cod EntityManager.HandleCollisions ().

 // manipulați coliziuni cu găuri negre pentru (int i = 0; i < blackHoles.Count; i++)  for (int j = 0; j < enemies.Count; j++) if (enemies[j].IsActive && IsColliding(blackHoles[i], enemies[j])) enemies[j].WasShot(); for (int j = 0; j < bullets.Count; j++)  if (IsColliding(blackHoles[i], bullets[j]))  bullets[j].IsExpired = true; blackHoles[i].WasShot();   if (IsColliding(PlayerShip.Instance, blackHoles[i]))  KillPlayer(); break;  

În cele din urmă, deschideți EnemySpawner clasă și să creeze niște găuri negre. Am limitat numărul maxim de găuri negre la două și am dat o șansă de 1 la 600 de găuri negre care au dat naștere fiecărui cadru.

 dacă (EntityManager.BlackHoleCount < 2 && rand.Next((int)inverseBlackHoleChance) == 0) EntityManager.Add(new BlackHole(GetSpawnPosition()));

Concluzie

Am adăugat bloom folosind diferite shaders, și găuri negre folosind formule diferite de forță. Shape Blaster începe să arate destul de bine. În următoarea parte, vom adăuga unele nebun, peste efectele de particule de top.