Gör en Neon Vector Shooter i XNA Bloom och Black Holes

I den här serien av handledningar visar jag dig hur man gör en neon tvillingskytt, som Geometry Wars, i XNA. Målet med dessa handledningar är att inte lämna dig en exakt kopia av Geometry Wars, utan snarare att gå över de nödvändiga elementen som gör att du kan skapa din egen högkvalitativa variant.


Översikt

I serien hittills har vi satt upp den grundläggande gameplayen för vår neon twin stick shooter, Shape Blaster. I denna handledning kommer vi att skapa signatur neon look genom att lägga till ett blommat efterbehandlingsfilter.

Varning: Högt!

Enkla effekter som denna eller partikeleffekter kan göra ett spel betydligt mer tilltalande utan att det behövs några förändringar i spelningen. Effektiv användning av visuella effekter är ett viktigt övervägande i vilket spel som helst. Efter att du har lagt till blomfiltret lägger vi också till svarta hål i spelet.


Bloom Post-Processing Effect

Bloom beskriver effekten du ser när du tittar på ett objekt med ett starkt ljus bakom det och ljuset blöder över objektet. I Shape Blaster kommer blomningseffekten att göra de ljusa linjerna på fartygen och partiklarna ser ut som ljusa, glödande, neonljus.

Solljus blommar genom träden

För att applicera blomning i vårt spel måste vi göra vår scen till ett resultatmål och använd sedan vårt blomfilter till det givna målet.

Bloom arbetar i tre steg:

  1. Extrahera de ljusa delarna av bilden.
  2. Blur de ljusa delarna.
  3. Rekombinera den suddiga bilden med originalbilden när du gör lite ljusstyrka och mättnadsjusteringar.

Vart och ett av dessa steg kräver en shader - i huvudsak ett kort program som körs på ditt grafikkort. Shaders i XNA är skrivna i ett speciellt språk som heter High Level Shader Language (HLSL). Provbilden nedan visar resultatet av varje steg.

Initial bild De ljusa områdena extraheras från bilden De ljusa områdena efter suddning Slutresultatet efter rekombination med originalbilden

Lägger till Bloom till Shape Blaster

För vårt blomfilter använder vi XNA Bloom Postprocess Sample.

Att integrera blomprovet med vårt projekt är enkelt. Hitta först de två kodfilerna ur urvalet, BloomComponent.cs och BloomSettings.cs, och lägg till dem till ShapeBlaster projekt. Lägg även till BloomCombine.fx, BloomExtract.fx, och GaussianBlur.fx till innehållsrörledningsprojektet.

I GameRoot, Lägg till en använder sig av uttalande för BloomPostprocess namespace och lägg till en BloomComponent medlemsvariabel.

 BloomComponent bloom;

I GameRoot konstruktör, lägg till följande rader.

 blom = ny BloomComponent (detta); Components.Add (bloom); bloom.Settings = nya BloomSettings (null, 0.25f, 4, 2, 1, 1.5f, 1);

Slutligen, i början av GameRoot.Draw (), lägg till följande rad.

 bloom.BeginDraw ();

Det är allt. Om du kör spelet nu bör du se blomningen i kraft.

När du ringer bloom.BeginDraw (), det omdirigerar efterföljande rita samtal till ett resultatmål som blomningen ska tillämpas på. När du ringer base.Draw () i slutet av GameRoot.Draw () metod, den BloomComponent's Dra() Metoden heter. Det är här blomman appliceras och scenen dras till bakbufferten. Därför måste allt som behöver ha blommat appliceras dras mellan samtalen till bloom.BeginDraw () och base.Draw ().

Tips: Om du vill rita något utan blomning (till exempel användargränssnittet), rita det efter anropet till base.Draw ().

Du kan justera blominställningarna efter eget tycke. Jag har valt följande värden:

  • 0,25 för bloom tröskeln. Det betyder att delar av bilden som är mindre än en fjärdedel av full ljusstyrka inte bidrar till att blomstra.
  • 4 för suddemängden. För den matematiska benägenheten är detta standardavvikelsen för Gaussens oskärpa. Större värden blur ljuset blommar mer. Tänk dock på att oskärpa shader är inställd att använda ett fast antal prover, oavsett oskärpa. Om du ställer in det här värdet för högt, kommer suddningen att sträcka sig bortom den radie som kommer från shaderproverna och artefakterna. Idealiskt bör detta värde inte vara mer än en tredjedel av din provtagningsradie för att säkerställa att felet är försumbart.
  • 2 för blomintensiteten, som bestämmer hur starkt blomman påverkar slutresultatet.
  • 1 för basintensiteten, som bestämmer hur starkt den ursprungliga bilden påverkar slutresultatet.
  • 1,5 för blommans mättnad. Detta medför att glansen runt ljusa föremål har mer mättade färger än föremålen själva. Ett högt värde valdes för att simulera utseendet på neonljus. Om du tittar på mitten av ett starkt neonljus, ser det nästan vitt ut, medan glansen runt den är starkare färgad.
  • 1 för basmättnaden. Detta värde påverkar mättnaden av basbilden.
Utan blomma Med blom

Bloom under huven

Blomfiltret är implementerat i BloomComponent klass. Blomkomponenten börjar genom att skapa och ladda de nödvändiga resurserna i sin LoadContent () metod. Här laddar den de tre shaders det kräver och skapar tre gör mål.

Det första gör målet, sceneRenderTarget, är för att hålla den scen som blomman kommer att appliceras på. De andra två, renderTarget1 och renderTarget2, används för att tillfälligt hålla mellanförhållandena mellan varje reningspass. Dessa gör mål gör halvdelen av spelets upplösning för att minska prestationskostnaden. Detta minskar inte blommans slutliga kvalitet, eftersom vi i alla fall kommer att blura de blomstra bilderna.

Bloom kräver fyra reningspass, som visas i detta diagram:

I XNA, den Effekt klassen inkapslar en skuggare. Du skriver koden för shader i separat fil, som du lägger till i innehållsrörledningen. Det här är filerna med .fx förlängning vi lagt till tidigare. Du laddar skuggaren i en Effekt objekt genom att ringa Content.Load() metod i LoadContent (). Det enklaste sättet att använda en shader i ett 2D-spel är att passera Effekt objekt som en parameter till SpriteBatch.Begin ().

Det finns flera typer av shaders, men för blomfiltret kommer vi bara att använda pixel shaders (kallas ibland fragment shaders). En pixel shader är ett litet program som kör en gång för varje pixel du ritar och bestämmer pixelens färg. Vi ska gå över var och en av de använda shadersna.

De BloomExtract Shader

De BloomExtract shader är den enklaste av de tre shadersna. Dess jobb är att extrahera områdena av bilden som är ljusare än vissa trösklar och sedan skala färgvärdena för att använda fullfärgsområdet. Alla värden under tröskeln blir svarta.

Den fullständiga skuggkoden visas nedan.

 sampler TextureSampler: register (s0); float BloomThreshold; float4 PixelShaderFunction (float2 texCoord: TEXCOORD0): COLOR0 // Slå upp den ursprungliga bildfärgen. float4 c = tex2D (TextureSampler, texCoord); // Justera det för att hålla bara värden ljusare än den angivna tröskeln. returnera mättnad ((c - BloomThreshold) / (1 - BloomThreshold));  teknik BloomExtract pass Pass1 PixelShader = kompilera ps_2_0 PixelShaderFunction (); 

Oroa dig inte om du inte är bekant med HLSL. Låt oss undersöka hur det här fungerar.

 sampler TextureSampler: register (s0);

Denna första del deklarerar en texturprovtagare som heter TextureSampler. SpriteBatch kommer att binda en textur till denna provtagare när den drar med denna skuggare. Ange vilket register du vill binda till är valfritt. Vi använder provtagaren för att leta upp pixlar från den bundna strukturen.

 float BloomThreshold;

BloomThreshold är en parameter som vi kan ställa in från vår C # -kod.

 float4 PixelShaderFunction (float2 texCoord: TEXCOORD0): COLOR0 

Det här är vår pixel shader funktion deklaration som tar texturkoordinater som inmatning och returnerar en färg. Färgen returneras som en float4. Detta är en samling av fyra flottor, ungefär som a Vector4 i XNA. De lagrar färgens röda, gröna, blå och alfakomponenter som värden mellan noll och en.

TEXCOORD0 och COLOR0 kallas semantik, och de anger för kompilatorn hur texCoord parametern och returvärdet används. För varje pixelutgång, texCoord kommer att innehålla koordinaterna för motsvarande punkt i ingående texturen med (0, 0) att vara det övre vänstra hörnet och (1, 1) att vara längst ned till höger.

 // Se upp den ursprungliga bildfärgen. float4 c = tex2D (TextureSampler, texCoord); // Justera det för att hålla bara värden ljusare än den angivna tröskeln. returnera mättnad ((c - BloomThreshold) / (1 - BloomThreshold));

Här är allt det verkliga arbetet gjort. Det hämtar pixelfärgen från texturen, subtraherar BloomThreshold från varje färgkomponent och sedan skalar den upp igen så att det maximala värdet är ett. De mätta() funktionen klämmer sedan färgens komponenter mellan noll och en.

Du märker kanske det c och BloomThreshold är inte samma typ som c är en float4 och BloomThreshold är en flyta. HLSL tillåter dig att göra operationer med dessa olika typer genom att väsentligen vrida flyta in i en float4 med alla komponenter samma. (c - BloomThreshold) blir effektivt:

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

Resten av skuggaren skapar helt enkelt en teknik som använder pixel shader-funktionen, sammanställd för shader model 2.0.

De gaussisk oskärpa Shader

En Gaussisk oskärpa försvinner en bild med en Gaussisk funktion. För varje pixel i utmatningsbilden summerar vi pixlarna i inmatningsbilden viktad av deras avstånd från målpunkten. Närliggande bildpunkter bidrar mycket till den slutliga färgen medan avlägsna bildpunkter bidrar mycket lite.

Eftersom avlägsna pixlar gör försumliga bidrag och eftersom texturuppslag är kostsamma, samlar vi bara pixlar inom en kort radie istället för att prova hela texturen. Denna skuggare kommer att prova poäng inom 14 pixlar av den aktuella pixeln.

Ett naivt genomförande kan prova alla punkter i en kvadrat runt den aktuella pixeln. Detta kan dock vara dyrt. I vårt exempel måste vi prova poäng inom en 29x29 kvadrat (14 poäng på vardera sidan av pixeln, plus mittpunkten). Det är totalt 841 prover för varje bildpunkt i vår bild. Lyckligtvis finns det en snabbare metod. Det visar sig att en 2D Gaussisk oskärpa motsvarar att du först suddar bilden horisontellt och sedan suddar den igen vertikalt. Var och en av dessa endimensionella oskärpa kräver endast 29 prover, vilket minskar vår totala till 58 prover per pixel.

Ytterligare ett knep används för att ytterligare öka effektiviteten hos suddningen. När du berättar GPU att prova mellan två pixlar, kommer det att returnera en blandning av de två pixlarna utan ytterligare prestanda. Eftersom vår oskärpa blandar pixlar ihop ändå tillåter vi oss att prova två pixlar i taget. Detta sänker antalet nödvändiga prover nästan i hälften.

Nedan finns relevanta delar av gaussisk oskärpa 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; // Kombinera ett antal viktiga bildfilterkranar. för (int i = 0; i < SAMPLE_COUNT; i++)  c += tex2D(TextureSampler, texCoord + SampleOffsets[i]) * SampleWeights[i];  return c; 

Skuggaren är faktiskt ganska enkel; det tar bara en uppsättning offsets och en motsvarande mängd vikter och beräknar den viktade summan. Allt komplex matte är faktiskt i C # -koden som fyller offset- och viktuppsättningarna. Detta görs i SetBlurEffectParameters () och ComputeGaussian () metoder för BloomComponent klass. När du utför det horisontella suddpasset, SampleOffsets kommer att fyllas med endast horisontella förskjutningar (y-komponenterna är alla noll) och naturligtvis är omvänden sant för det vertikala passet.

De BloomCombine Shader

De BloomCombine shader gör ett par saker på en gång. Den kombinerar blomtexturen med den ursprungliga texturen samtidigt som man anpassar intensiteten och mättnaden av varje textur.

Shader börjar genom att deklarera två texturprovtagare och fyra floatparametrar.

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

En sak att notera är det SpriteBatch kommer automatiskt binda texturen du skickar den när du ringer SpriteBatch.Draw () till första samplaren, men det kommer inte automatiskt binda något till den andra samplaren. Den andra samplaren ställs in manuellt i BloomComponent.Draw () med följande rad.

 GraphicsDevice.Textures [1] = sceneRenderTarget;

Därefter har vi en hjälparfunktion som justerar mättnaden av en färg.

 float4 AdjustSaturation (float4 färg, flytmättnad) // Konstanterna 0.3, 0.59 och 0.11 väljs eftersom // människans öga är mer känsligt för grönt ljus och mindre till blått. float grå = punkt (färg, float3 (0,3, 0,59, 0,11)); returnera lerp (grå, färg, mättnad); 

Denna funktion tar en färg och ett mättnadsvärde och ger en ny färg. Passerar en mättnad av 1 lämnar färgen oförändrad. Godkänd 0 kommer att återgå grå, och passande värden större än en kommer att returnera en färg med ökad mättnad. Att skicka negativa värden är verkligen utanför den avsedda användningen, men kommer att invertera färgen om du gör det.

Funktionen fungerar genom att först finna ljusets ljusstyrka genom att ta en viktad summa baserat på våra ögons känslighet för rött, grönt och blått ljus. Den interpolerar sedan linjärt mellan grå och ursprunglig färg med angiven mängd mättnad. Denna funktion kallas av pixel shader-funktionen.

 float4 PixelShaderFunction (float2 texCoord: TEXCOORD0): COLOR0 // Slå upp blomningen och originalbasens bildfärger. float4 bloom = tex2D (BloomSampler, texCoord); float4 bas = tex2D (BaseSampler, texCoord); // Justera färgmättnad och intensitet. bloom = AdjustSaturation (blom, BloomSaturation) * BloomIntensity; bas = AdjustSaturation (bas, BaseSaturation) * BaseIntensity; // Mörk ner basbilden i områden där det finns mycket blomstring, // för att förhindra att saker ser ut överdrivet utbränd. bas * = (1 - mätta (blom)); // Kombinera de två bilderna. retur bas + blom; 

Återigen är denna skuggning ganska enkel. Om du undrar varför basbilden måste mörkas i områden med ljusblomning, kom ihåg att lägga till två färger tillsammans ökar ljusstyrkan och eventuella färgkomponenter som lägger till ett värde som är större än en (full ljusstyrka) kommer att klippas till en . Eftersom blombilden liknar basbilden, skulle detta leda till att mycket av bilden som har över 50% ljusstyrka blir maximal ut. Mörk basbilden kartlägger alla färger tillbaka till det sortiment av färger som vi kan korrekt visa.


Svarta hål

En av de mest intressanta fienderna i Geometry Wars är det svarta hålet. Låt oss undersöka hur vi kan göra något liknande i Shape Blaster. Vi kommer att skapa den grundläggande funktionaliteten nu, och vi kommer att återkomma fienden i nästa handledning för att lägga till partikel effekter och partikel interaktioner.

Ett svart hål med kretsande partiklar

Grundfunktionalitet

De svarta hålen kommer att dra in spelarens skepp, närliggande fiender, och (efter nästa handledning) partiklar, men kommer att avvärja kulor.

Det finns många möjliga funktioner vi kan använda för attraktion eller avstängning. Det enklaste är att använda konstant kraft så att det svarta hålet drar med samma styrka, oavsett objektets avstånd. Ett annat alternativ är att kraften ökar linjärt från noll vid ett visst maximalt avstånd till full styrka för objekt direkt ovanför det svarta hålet.

Om vi ​​skulle vilja modellera gravitationen mer realistiskt kan vi använda inversfältet av avståndet, vilket betyder att tyngdkraften är proportionell mot \ (1 / avstånd ^ 2 \). Vi använder faktiskt alla dessa tre funktioner för att hantera olika objekt. Kulorna kommer att avstötas med en konstant kraft, fienderna och spelarens skepp kommer att lockas med en linjär kraft och partiklarna kommer att använda en invers kvadratfunktion.

Vi gör en ny klass för svarta hål. Låt oss börja med grundläggande funktionalitet.

 klass BlackHole: Entity privat statisk Random rand = ny slumpmässig (); privata int hitpoints = 10; offentlig BlackHole (Vector2-position) image = Art.BlackHole; Position = position; Radius = image.Width / 2f;  offentligt ogiltigt WasShot () hitpoints--; om (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 svarta hålen tar tio skott för att döda. Vi justerar spritens skala en aning för att den ska pulseras. Om du bestämmer dig för att förstöra svarta hål bör du också ge poäng, du måste göra liknande anpassningar till Svart hål klass som vi gjorde med fiendens klass.

Nästa gör vi att de svarta hålen faktiskt tillämpar en kraft på andra enheter. Vi behöver en liten hjälpmetod från vår EntityManager.

 offentliga statiska IEnumerable GetNearbyEntities (Vector2 position, floatradie) return entities.Where (x => Vector2.DistanceSquared (position, x.Position) < radius * radius); 

Denna metod kan bli effektivare genom att använda ett mer komplicerat rumsligt partitioneringsschema, men för antalet enheter vi kommer att få, är det bra som det är. Nu kan vi göra de svarta hålen applicera kraft i deras Uppdatering() metod.

 offentlig åsidosätt annullering Uppdatering () var entities = EntityManager.GetNearbyEntities (Position, 250); foreach (var enhet i enheter) if (enhet är Enemy &&! (entity as Enemy) .IsActive) fortsätt; // kulor avstötas av svarta hål och allt annat lockas om (enhet är Bullet) entity.Velocity + = (entity.Position - Position) .ScaleTo (0.3f); annars var dPos = Position - entity.Position; var längd = dPos.Length (); entity.Velocity + = dPos.ScaleTo (MathHelper.Lerp (2, 0, längd / 250f)); 

Svarta hål påverkar endast enheter inom en vald radie (250 pixlar). Kulor inom denna radie har en konstant repulsiv kraft, medan allt annat har en linjär attraktiv kraft som appliceras.

Vi måste lägga till kollisionshantering för svarta hål till EntityManager. Lägg till en List <> för svarta hål som vi gjorde för de andra typerna av enheter, och lägg till följande kod i EntityManager.HandleCollisions ().

 // hantera kollisioner med svarta hål för (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;  

Slutligen öppna EnemySpawner klass och få det att skapa några svarta hål. Jag begränsade det maximala antalet svarta hål till två och gav en 1 till 600 chans att ett svart hål skulle gyta varje ram.

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

Slutsats

Vi har lagt till blom med olika shaders och svarta hål med olika kraftformler. Shape Blaster börjar se ganska bra ut. I nästa del lägger vi till några galna, över de bästa partikeleffekterna.