Maak een Neon Vector Shooter in XNA Bloom en zwarte gaten

In deze serie tutorials laat ik je zien hoe je een neon-tweepijps-schietspel maakt, zoals Geometry Wars, in XNA. Het doel van deze tutorials is om je niet te laten met een exacte replica van Geometry Wars, maar om de nodige elementen door te nemen die je in staat stellen om je eigen hoogwaardige variant te maken.


Overzicht

In de serie tot nu toe hebben we de basisgameplay voor onze neon twin-stick shooter Shape Blaster ingesteld. In deze zelfstudie maken we de kenmerkende neonlook door een filter voor naverwerking in bloei toe te voegen.

Waarschuwing: Loud!

Eenvoudige effecten zoals deze of partikeleffecten kunnen een game aanzienlijk aantrekkelijker maken zonder dat het spel moet worden gewijzigd. Effectief gebruik van visuele effecten is een belangrijke overweging in elk spel. Na het toevoegen van het bloom-filter voegen we ook zwarte gaten toe aan het spel.


Bloom Post-Processing Effect

Bloom beschrijft het effect dat je ziet als je naar een object kijkt met een helder licht erachter en het lijkt of het licht boven het object uitloopt. In Shape Blaster zorgt het bloom-effect ervoor dat de heldere lijnen van de schepen en deeltjes eruitzien als heldere, gloeiende neonlichten.

Zonlicht dat door de bomen bloeit

Om bloeien toe te passen in onze game, moeten we onze scène renderen naar een renderdoel en vervolgens onze bloeifilter toepassen op dat renderdoel.

Bloom werkt in drie stappen:

  1. Pak de heldere delen van de afbeelding uit.
  2. Maak de heldere delen onscherp.
  3. De vervaagde afbeelding opnieuw combineren met de originele afbeelding terwijl u bepaalde helderheids- en verzadigingsaanpassingen uitvoert.

Voor elk van deze stappen is een shader - in wezen een kort programma dat op uw grafische kaart wordt uitgevoerd. Shaders in XNA worden geschreven in een speciale taal genaamd High-Level Shader Language (HLSL). De onderstaande voorbeeldafbeeldingen tonen het resultaat van elke stap.

Eerste afbeelding De heldere gebieden worden uit de afbeelding gehaald De heldere delen na vervaging Het eindresultaat na het recombineren met het originele beeld

Bloom toevoegen aan Shape Blaster

Voor ons bloeifilter zullen we het XNA Bloom Postprocess-monster gebruiken.

Het integreren van de bloeisample met ons project is eenvoudig. Zoek eerst de twee codebestanden uit het voorbeeld, BloomComponent.cs en BloomSettings.cs, en voeg ze toe aan de ShapeBlaster project. Voeg ook toe BloomCombine.fx, BloomExtract.fx, en GaussianBlur.fx naar het content pipeline project.

In GameRoot, Voeg een ... toe gebruik makend van verklaring voor de BloomPostprocess naamruimte en voeg een toe BloomComponent lidvariabele.

 BloomComponent bloei;

In de GameRoot constructor, voeg de volgende regels toe.

 bloom = nieuw BloomComponent (dit); Components.Add (standaard); bloom.Settings = nieuwe BloomSettings (null, 0.25f, 4, 2, 1, 1.5f, 1);

Eindelijk, helemaal aan het begin van GameRoot.Draw (), voeg de volgende regel toe.

 bloom.BeginDraw ();

Dat is het. Als je het spel nu uitvoert, zou je de bloei moeten zien.

Wanneer je belt bloom.BeginDraw (), het leidt toekomstige tekenoproepen om naar een weergavetarget waarop bloei wordt toegepast. Wanneer je belt base.Draw () aan het einde van de GameRoot.Draw () methode, de BloomComponent's Trek() methode wordt genoemd. Hier wordt de bloei toegepast en wordt de scène naar de achtergrondbuffer getrokken. Daarom moet alles wat nodig is, bloei toegepast hebben tussen de oproepen naar bloom.BeginDraw () en base.Draw ().

Tip: Als u iets zonder bloei wilt tekenen (bijvoorbeeld de gebruikersinterface), tekent u het na de oproep aan base.Draw ().

U kunt de bloom-instellingen naar wens aanpassen. Ik heb de volgende waarden gekozen:

  • 0.25 voor de bloeidrempel. Dit betekent dat delen van de afbeelding die minder dan een kwart van de volledige helderheid hebben, niet bijdragen aan de bloei.
  • 4 voor de vervaagde hoeveelheid. Voor wiskundigen is dit de standaarddeviatie van de Gaussiaanse vervaging. Grotere waarden vervagen de lichtbloei meer. Houd er echter rekening mee dat de blur-arcering is ingesteld om een ​​vast aantal samples te gebruiken, ongeacht de hoeveelheid vervaging. Als u deze waarde te hoog instelt, wordt de vervaging groter dan de straal vanaf waar de arceringsamples en artefacten worden weergegeven. Idealiter zou deze waarde niet meer dan een derde van uw bemonsteringsradius moeten zijn om ervoor te zorgen dat de fout te verwaarlozen is.
  • 2 voor de bloeiintensiteit, die bepaalt hoe sterk de bloei het eindresultaat beïnvloedt.
  • 1 voor de basisintensiteit, die bepaalt hoe sterk het oorspronkelijke beeld het eindresultaat beïnvloedt.
  • 1.5 voor de bloeiverzadiging. Dit zorgt ervoor dat de gloed rond heldere objecten meer verzadigde kleuren heeft dan de objecten zelf. Er is gekozen voor een hoge waarde om het uiterlijk van neonlichten te simuleren. Als je naar het midden van een fel neonlicht kijkt, ziet het er bijna wit uit, terwijl de gloed eromheen sterker gekleurd is.
  • 1 voor de basisverzadiging. Deze waarde beïnvloedt de verzadiging van de basisafbeelding.
Zonder bloei Met bloei

Bloei onder de motorkap

Het bloom-filter is geïmplementeerd in de BloomComponent klasse. De bloom-component begint met het maken en laden van de benodigde bronnen in zijn LoadContent () methode. Hier worden de drie shaders geladen die nodig zijn en worden drie renderdoelen gemaakt.

Het eerste renderdoel, sceneRenderTarget, is voor het houden van de scène waarop de bloei wordt toegepast. De andere TWEE, renderTarget1 en renderTarget2, worden gebruikt om de tussentijdse resultaten tijdelijk tussen elke renderpas te houden. Deze renderdoelen zijn gemaakt voor de helft van de resolutie van het spel om de prestatiekosten te verlagen. Dit vermindert de uiteindelijke kwaliteit van de bloei niet, omdat we de bloeibeelden toch zullen vervagen.

Bloom heeft vier rendering passes nodig, zoals weergegeven in dit diagram:

In XNA, de Effect klasse kapselt een arcering in. U schrijft de code voor de arcering in een afzonderlijk bestand dat u toevoegt aan de inhoudspijplijn. Dit zijn de bestanden met de .fx uitbreiding die we eerder hebben toegevoegd. U laadt de arcering in een Effect object door de Content.Load() methode in LoadContent (). De eenvoudigste manier om een ​​arcering in een 2D-spel te gebruiken, is door de Effect object als een parameter voor SpriteBatch.Begin ().

Er zijn verschillende soorten shaders, maar voor het bloom-filter dat we alleen gebruiken pixel shaders (soms genoemd fragment shaders). Een pixel-arcering is een klein programma dat eenmaal wordt uitgevoerd voor elke pixel die u tekent en de kleur van de pixel bepaalt. We zullen elk van de gebruikte shaders bespreken.

De BloomExtract Shader

De BloomExtract shader is de eenvoudigste van de drie shaders. Het is zijn taak om de delen van de afbeelding die helderder zijn dan een drempel uit te pakken en vervolgens de kleurwaarden in te schalen om het volledige kleurenbereik te gebruiken. Alle waarden onder de drempelwaarde worden zwart.

De volledige arceringscode wordt hieronder weergegeven.

 sampler TextureSampler: register (s0); floep BloomThreshold; float4 PixelShaderFunction (float2 texCoord: TEXCOORD0): COLOR0 // Zoek de originele afbeeldingskleur op. float4 c = tex2D (TextureSampler, texCoord); // Pas het aan om alleen de waarden helderder dan de opgegeven drempel te houden. return saturate ((c - BloomThreshold) / (1 - BloomThreshold));  techniek BloomExtract pass Pass1 PixelShader = compileer ps_2_0 PixelShaderFunction (); 

Maak je geen zorgen als je niet bekend bent met HLSL. Laten we eens kijken hoe dit werkt.

 sampler TextureSampler: register (s0);

Dit eerste deel verklaart een texture sampler genaamd TextureSampler. SpriteBatch zal een textuur aan deze sampler binden wanneer deze met deze arcering tekent. Het opgeven van welk register u wilt binden, is optioneel. We gebruiken de sampler om pixels op te zoeken uit de gebonden textuur.

 floep BloomThreshold;

BloomThreshold is een parameter die we kunnen instellen met onze C # -code.

 float4 PixelShaderFunction (float2 texCoord: TEXCOORD0): COLOR0 

Dit is onze pixeldehader-functieaangifte die textuurcoördinaten als invoer neemt en een kleur retourneert. De kleur wordt geretourneerd als een float4. Dit is een verzameling van vier drijvers, ongeveer zoals een Vector4 in XNA. Ze slaan de rode, groene, blauwe en alpha-componenten van de kleur op als waarden tussen nul en één.

TEXCOORD0 en COLOR0 worden genoemd semantiek, en zij geven aan de compiler aan hoe het texCoord parameter en de retourwaarde worden gebruikt. Voor elke pixeluitvoer, texCoord bevat de coördinaten van het overeenkomstige punt in de invoerstructuur, met (0, 0) de linkerbovenhoek en (1, 1) rechtsonder.

 // Zoek de originele afbeeldingskleur op. float4 c = tex2D (TextureSampler, texCoord); // Pas het aan om alleen de waarden helderder dan de opgegeven drempel te houden. return saturate ((c - BloomThreshold) / (1 - BloomThreshold));

Dit is waar al het echte werk wordt gedaan. Het haalt de pixelkleur uit de textuur, trekt af BloomThreshold van elke kleurcomponent en schaalt deze vervolgens weer omhoog zodat de maximale waarde één is. De verzadigen() functie klemt de componenten van de kleur vervolgens tussen nul en één.

Dat mag je opvallen c en BloomThreshold zijn niet hetzelfde type, zoals c is een float4 en BloomThreshold is een vlotter. Met HLSL kunt u bewerkingen met deze verschillende typen uitvoeren door de knop in wezen te draaien vlotter in een float4 met alle componenten hetzelfde. (c - BloomThreshold) wordt effectief:

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

De rest van de arcering maakt eenvoudigweg een techniek die de pixel shader-functie gebruikt, gecompileerd voor shader-model 2.0.

De GaussianBlur Shader

Een Gaussiaanse vervaging vervaagt een afbeelding met behulp van een Gauss-functie. Voor elke pixel in het uitvoerbeeld vatten we de pixels in het invoerbeeld samen, gewogen naar hun afstand tot het doelpixel. Pixels in de buurt dragen sterk bij aan de uiteindelijke kleur, terwijl pixels op afstand heel weinig bijdragen.

Omdat verre pixels verwaarloosbare bijdragen leveren en omdat textuuropzoekingen duur zijn, samplen we alleen pixels in een korte straal in plaats van de hele textuur te samplen. Deze arcering bemonstert punten binnen 14 pixels van de huidige pixel.

Een naïeve implementatie kan alle punten in een vierkant rond de huidige pixel samplen. Dit kan echter kostbaar zijn. In ons voorbeeld zouden we punten moeten bemonsteren binnen een vierkant van 29x29 (14 punten aan weerszijden van het middelste pixel plus de middelste pixel). Dat is een totaal van 841 voorbeelden voor elke pixel in onze afbeelding. Gelukkig is er een snellere methode. Het blijkt dat het doen van een 2D Gaussiaanse vervaging gelijk is aan het eerst horizontaal vervagen van het beeld en het vervolgens verticaal vervagen. Elk van deze eendimensionale vervagingen vereist slechts 29 monsters, waardoor ons totaal tot 58 monsters per pixel vermindert.

Nog een truc wordt gebruikt om de efficiëntie van de vervaging verder te vergroten. Wanneer u de GPU laat bemonsteren tussen twee pixels, retourneert deze een combinatie van de twee pixels zonder extra prestatiekosten. Omdat onze vervaging pixels toch samen mengt, kunnen we twee pixels tegelijkertijd samplen. Hierdoor wordt het aantal vereiste monsters bijna gehalveerd.

Hieronder staan ​​de relevante delen van de GaussianBlur shader.

 sampler TextureSampler: register (s0); #define SAMPLE_COUNT 15 float2 SampleOffsets [SAMPLE_COUNT]; float SampleWeights [SAMPLE_COUNT]; float4 PixelShaderFunction (float2 texCoord: TEXCOORD0): COLOR0 float4 c = 0; // Combineer een aantal gewogen afbeeldingsfiltertaps. voor (int i = 0; i < SAMPLE_COUNT; i++)  c += tex2D(TextureSampler, texCoord + SampleOffsets[i]) * SampleWeights[i];  return c; 

De shader is eigenlijk vrij eenvoudig; het kost slechts een reeks verschuivingen en een overeenkomstige reeks gewichten en berekent de gewogen som. Alle complexe wiskunde bevindt zich eigenlijk in de C # -code die de offset- en gewichtsmatrices bevat. Dit wordt gedaan in de SetBlurEffectParameters () en ComputeGaussian () methoden van de BloomComponent klasse. Bij het uitvoeren van de horizontale blur pass, SampleOffsets wordt gevuld met alleen horizontale verschuivingen (de y-componenten zijn allemaal nul) en natuurlijk geldt het omgekeerde voor de verticale pas.

De BloomCombine Shader

De BloomCombine shader doet een paar dingen tegelijk. Het combineert de bloeirentextuur met de oorspronkelijke textuur en past tegelijkertijd de intensiteit en verzadiging van elke textuur aan.

De arcering begint met het declareren van twee texture-samplers en vier float-parameters.

 sampler BloomSampler: register (s0); sampler BaseSampler: register (s1); float BloomIntensity; float BaseIntensity; zweven BloomSaturation; float BaseSaturation;

Een ding om op te merken is dat SpriteBatch zal automatisch de textuur binden die u tijdens het bellen doorgeeft SpriteBatch.Draw () naar de eerste sampler, maar deze zal niet automatisch iets aan de tweede sampler binden. De tweede sampler wordt handmatig ingesteld BloomComponent.Draw () met de volgende regel.

 GraphicsDevice.Textures [1] = sceneRenderTarget;

Vervolgens hebben we een helperfunctie die de verzadiging van een kleur aanpast.

 float4 AdjustSaturation (float4-kleur, float-verzadiging) // De constanten 0,3, 0,59 en 0,11 worden gekozen omdat het // menselijke oog gevoeliger is voor groen licht en minder voor blauw. float grijs = punt (kleur, float3 (0,3, 0,59, 0,11)); return lerp (grijs, kleur, verzadiging); 

Deze functie neemt een kleur en een verzadigingswaarde en retourneert een nieuwe kleur. Een verzadiging passeren van 1 laat de kleur onveranderd. Passing 0 wordt grijs weergegeven en als waarden groter dan één worden doorgegeven, krijgt u een kleur met een hogere verzadiging. Het doorgeven van negatieve waarden valt echt buiten het bedoelde gebruik, maar zal de kleur omkeren als u dat doet.

De functie werkt door eerst de helderheid van de kleur te vinden door een gewogen som te nemen op basis van de gevoeligheid van onze ogen voor rood, groen en blauw licht. Vervolgens interpoleert het lineair tussen grijs en de oorspronkelijke kleur met de opgegeven hoeveelheid verzadiging. Deze functie wordt aangeroepen door de pixel shader-functie.

 float4 PixelShaderFunction (float2 texCoord: TEXCOORD0): COLOR0 // Zoek de bloom en originele basisbeeldkleuren op. float4 bloom = tex2D (BloomSampler, texCoord); float4 base = tex2D (BaseSampler, texCoord); // Pas kleurverzadiging en -intensiteit aan. bloom = AdjustSaturation (bloom, BloomSaturation) * BloomIntensity; base = AdjustSaturation (base, BaseSaturation) * BaseIntensity; // Maak het basisbeeld donkerder in gebieden met veel bloei, // om te voorkomen dat dingen er uitgebrand uitzien. base * = (1 - verzadiging (bloom)); // Combineer de twee afbeeldingen. terugkeerbasis + bloei; 

Nogmaals, deze arcering is vrij eenvoudig. Als je je afvraagt ​​waarom de basisafbeelding moet worden verduisterd in gebieden met felle bloei, onthoud dan dat het toevoegen van twee kleuren samen de helderheid verhoogt en dat alle kleurcomponenten die samen een waarde groter dan één (volledige helderheid) vormen, worden geknipt tot één . Omdat het bloom-beeld vergelijkbaar is met het basisbeeld, zou dit ertoe leiden dat veel van het beeld met meer dan 50% helderheid wordt uitgezet. Door het basisbeeld donkerder te maken, worden alle kleuren opnieuw in kaart gebracht in het kleurenbereik dat we correct kunnen weergeven.


Zwarte gaten

Een van de meest interessante vijanden in Geometry Wars is het zwarte gat. Laten we eens kijken hoe we iets vergelijkbaars kunnen maken in Shape Blaster. We zullen nu de basisfunctionaliteit maken en we zullen de vijand in de volgende tutorial opnieuw bezoeken om deeltjeseffecten en deeltjesinteracties toe te voegen.

Een zwart gat met deeltjes in een baan om de aarde

Basisfunctionaliteit

De zwarte gaten zullen het schip van de speler, nabije vijanden en (na de volgende tutorial) deeltjes trekken, maar zullen kogels afstoten.

Er zijn veel mogelijke functies die we kunnen gebruiken voor aantrekking of afstoting. Het eenvoudigste is om constante kracht te gebruiken, zodat het zwarte gat met dezelfde kracht trekt, ongeacht de afstand van het object. Een andere optie is om de kracht lineair te laten toenemen van nul op een maximale afstand tot de volledige sterkte voor objecten direct bovenop het zwarte gat.

Als we de zwaartekracht meer realistisch willen modelleren, kunnen we het inverse kwadraat van de afstand gebruiken, wat betekent dat de zwaartekracht evenredig is aan \ (1 / afstand ^ 2 \). We zullen eigenlijk elk van deze drie functies gebruiken om verschillende objecten te behandelen. De kogels worden met een constante kracht afgestoten, de vijanden en het schip van de speler worden aangetrokken met een lineaire kracht, en de deeltjes zullen een omgekeerde vierkante functie gebruiken.

We zullen een nieuwe les maken voor zwarte gaten. Laten we beginnen met de basisfunctionaliteit.

 class BlackHole: Entity private static Random rand = new Random (); private int hitpoints = 10; public BlackHole (Vector2-positie) image = Art.BlackHole; Positie = positie; Radius = image.Width / 2f;  openbare leegte WasShot () hitpoints--; als (hitpoints <= 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);  

De zwarte gaten nemen tien schoten om te doden. We passen de schaal van de sprite enigszins aan om het te laten pulseren. Als u besluit dat het vernietigen van zwarte gaten ook punten moet toestaan, moet u vergelijkbare aanpassingen aanbrengen in de BlackHole klasse zoals we deden met de vijandelijke klasse.

Vervolgens zullen de zwarte gaten daadwerkelijk een kracht op andere entiteiten toepassen. We hebben een kleine hulpmethode nodig van onze EntityManager.

 public static IEnumerable GetNearbyEntities (Vector2-positie, zwevende straal) return entities.Where (x => Vector2.DistanceSquared (position, x.Position) < radius * radius); 

Deze methode kan efficiënter worden gemaakt door een meer gecompliceerd ruimtelijk partitioneringsschema te gebruiken, maar voor het aantal entiteiten dat we zullen hebben, is het prima zoals het is. Nu kunnen we de zwarte gaten kracht laten gebruiken in hun Bijwerken() methode.

 openbare overschrijving ongeldig Update () var entities = EntityManager.GetNearbyEntities (Position, 250); foreach (var entity in entities) if (entity is Enemy &&! (entity as Enemy) .IsActive) ga verder; // kogels worden afgestoten door zwarte gaten en al het andere wordt aangetrokken als (entity is Bullet) entity.Velocity + = (entity.Position - Position) .ScaleTo (0.3f); else var dPos = Positie - entity.Position; var length = dPos.Length (); entity.Velocity + = dPos.ScaleTo (MathHelper.Lerp (2, 0, length / 250f)); 

Zwarte gaten zijn alleen van invloed op entiteiten binnen een gekozen straal (250 pixels). Kogels binnen deze straal hebben een constante afstotende kracht toegepast, terwijl al het andere een lineaire aantrekkende kracht uitoefent.

We moeten botsingen voor zwarte gaten toevoegen aan de EntityManager. Voeg een ... toe List <> voor zwarte gaten zoals we deden voor de andere typen entiteiten en voeg de volgende code toe EntityManager.HandleCollisions ().

 // omgaan met botsingen met zwarte gaten voor (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;  

Open ten slotte de EnemySpawner klasse en laat het een aantal zwarte gaten maken. Ik beperkte het maximale aantal zwarte gaten tot twee, en gaf een kans van 1 op 600 op een zwart gat dat elk frame spaweed.

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

Conclusie

We hebben bloom toegevoegd met verschillende shaders en zwarte gaten met verschillende force-formules. Shape Blaster begint er redelijk goed uit te zien. In het volgende deel zullen we wat gekke, overdadige deeltjeseffecten toevoegen.