Faire un vecteur de tir néon dans XNA Bloom et Black Holes

Dans cette série de didacticiels, je vais vous montrer comment créer un jeu de tir au néon, tel que Geometry Wars, dans XNA. Le but de ces tutoriels n'est pas de vous laisser avec une réplique exacte de Geometry Wars, mais plutôt de passer en revue les éléments nécessaires qui vous permettront de créer votre propre variante de haute qualité..


Vue d'ensemble

Jusqu'à présent, dans la série, nous avons mis en place le gameplay de base de notre jeu de tir double stick néon, Shape Blaster. Dans ce tutoriel, nous allons créer le look néon signature en ajoutant un filtre de post-traitement Bloom..

Avertissement: fort!

Des effets simples tels que ceux-ci ou des effets de particules peuvent rendre un jeu considérablement plus attrayant sans nécessiter de modification du gameplay. L'utilisation efficace des effets visuels est une considération importante dans tout jeu. Après avoir ajouté le filtre bloom, nous allons également ajouter des trous noirs au jeu..


Effet post-traitement des fleurs

Bloom décrit l'effet que vous voyez lorsque vous regardez un objet avec une lumière vive derrière lui et que la lumière semble saigner au-dessus de l'objet. Dans Shape Blaster, l’effet bloom rend les lignes lumineuses des navires et les particules semblables à des néons lumineux et brillants..

La lumière du soleil qui fleurit à travers les arbres

Pour appliquer bloom à notre jeu, nous devons rendre notre scène à une cible de rendu, puis appliquer notre filtre de bloom à cette cible de rendu..

Bloom fonctionne en trois étapes:

  1. Extraire les parties lumineuses de l'image.
  2. Flou des parties brillantes.
  3. Recombinez l'image floue avec l'image d'origine tout en effectuant certains réglages de luminosité et de saturation.

Chacune de ces étapes nécessite un shader - essentiellement un programme court qui s'exécute sur votre carte graphique. Les shaders dans XNA sont écrits dans un langage spécial appelé langage HLSL (High-Level Shader Language). Les exemples d'images ci-dessous montrent le résultat de chaque étape..

Image initiale Les zones lumineuses extraites de l'image Les zones claires après le flou Le résultat final après recombinaison avec l'image originale

Ajout de Bloom à Shape Blaster

Pour notre filtre de bloom, nous utiliserons l’échantillon de post-traitement Bloom de XNA..

L'intégration de l'échantillon de bloom à notre projet est simple. Tout d’abord, localisez les deux fichiers de code de l’exemple, BloomComponent.cs et BloomSettings.cs, et les ajouter à la ShapeBlaster projet. Ajoutez aussi BloomCombine.fx, BloomExtract.fx, et GaussianBlur.fx au projet de pipeline de contenu.

Dans GameRoot, ajouter un en utilisant déclaration pour le BloomPostprocess espace de noms et ajouter un BloomComponent variable membre.

 BloomComponent Bloom;

dans le GameRoot constructeur, ajoutez les lignes suivantes.

 bloom = new BloomComponent (this); Composants.Add (Bloom); bloom.Settings = nouveaux BloomSettings (null, 0.25f, 4, 2, 1, 1.5f, 1);

Enfin, au tout début de GameRoot.Draw (), ajouter la ligne suivante.

 bloom.BeginDraw ();

C'est tout. Si vous lancez le jeu maintenant, vous devriez voir la floraison en vigueur.

Quand vous appelez bloom.BeginDraw (), il redirige les appels de tirage ultérieurs vers une cible de rendu à laquelle bloom sera appliqué. Quand vous appelez base.Draw () à la fin de GameRoot.Draw () méthode, la BloomComponentde Dessiner() méthode est appelée. C'est à cet endroit que la floraison est appliquée et que la scène est dessinée dans la mémoire tampon arrière. Par conséquent, tout ce qui doit être épanoui doit être tracé entre les appels à bloom.BeginDraw () et base.Draw ().

Pointe: Si vous voulez dessiner quelque chose sans bloom (par exemple, l'interface utilisateur), dessinez-le après l'appel à base.Draw ().

Vous pouvez modifier les paramètres de floraison à votre convenance. J'ai choisi les valeurs suivantes:

  • 0,25 pour le seuil de floraison. Cela signifie que les parties de l'image qui ont moins d'un quart de la luminosité totale ne contribueront pas à la floraison..
  • 4 pour le montant flou. Pour les personnes ayant une inclinaison mathématique, il s'agit de l'écart type du flou gaussien. Des valeurs plus grandes brouillent davantage la lumière. Cependant, gardez à l'esprit que le flou est configuré pour utiliser un nombre d'échantillons fixe, quelle que soit la quantité de flou. Si vous définissez cette valeur sur une valeur trop élevée, le flou s'étendra au-delà du rayon d'échantillonnage du shader et des artefacts apparaîtront. Idéalement, cette valeur ne doit pas dépasser le tiers de votre rayon d'échantillonnage pour que l'erreur soit négligeable..
  • 2 pour l'intensité de la floraison, qui détermine dans quelle mesure la floraison affecte le résultat final.
  • 1 pour l'intensité de base, qui détermine dans quelle mesure l'image d'origine affecte le résultat final.
  • 1,5 pour la saturation de la floraison. Cela fait en sorte que la lueur autour des objets lumineux ait des couleurs plus saturées que les objets eux-mêmes. Une valeur élevée a été choisie pour simuler l'aspect des néons. Si vous regardez au centre d’un néon lumineux, il a une apparence presque blanche, tandis que la lueur autour est plus intense..
  • 1 pour la saturation en base. Cette valeur affecte la saturation de l'image de base.
Sans floraison Avec fleur

Bloom sous le capot

Le filtre de bloom est implémenté dans le BloomComponent classe. Le composant bloom commence par créer et charger les ressources nécessaires dans son LoadContent () méthode. Ici, il charge les trois shaders nécessaires et crée trois cibles de rendu..

La première cible de rendu, sceneRenderTarget, est destiné à contenir la scène sur laquelle la floraison sera appliquée. Les deux autres, renderTarget1 et renderTarget2, sont utilisés pour conserver temporairement les résultats intermédiaires entre chaque passe de rendu. Ces cibles de rendu sont réduites à la moitié de la résolution du jeu afin de réduire les coûts de performances. Cela ne réduit pas la qualité finale de la floraison, car nous allons de toute façon brouiller les images de la floraison..

Bloom nécessite quatre passes de rendu, comme indiqué dans ce diagramme:

Dans XNA, le Effet La classe encapsule un shader. Vous écrivez le code du shader dans un fichier séparé que vous ajoutez au pipeline de contenu. Ce sont les fichiers avec le .fx extension que nous avons ajoutée plus tôt. Vous chargez le shader dans un Effet objet en appelant le Content.Load() méthode en LoadContent (). Le moyen le plus simple d’utiliser un shader dans un jeu 2D est de passer le test Effet objet en tant que paramètre pour SpriteBatch.Begin ().

Il existe plusieurs types de shaders, mais pour le filtre bloom, nous utiliserons uniquement pixel shaders (appelé quelques fois shaders de fragments). Un pixel shader est un petit programme qui s'exécute une fois par pixel et détermine la couleur du pixel. Nous allons passer en revue chacun des shaders utilisés.

le BloomExtract Shader

le BloomExtract shader est le plus simple des trois shaders. Son travail consiste à extraire les zones de l’image plus claires que certains seuils, puis à redimensionner les valeurs de couleur pour utiliser toute la plage de couleurs. Toute valeur inférieure au seuil deviendra noire.

Le code complet du shader est présenté ci-dessous.

 échantillonneur TextureSampler: registre (s0); float BloomThreshold; float4 PixelShaderFunction (float2 texCoord: TEXCOORD0): COLOR0 // Recherche la couleur de l'image d'origine. float4 c = tex2D (TextureSampler, texCoord); // Ajustez-le pour ne garder que les valeurs plus claires que le seuil spécifié. return saturate ((c - BloomThreshold) / (1 - BloomThreshold));  technique BloomExtract pass Pass1 PixelShader = compiler ps_2_0 PixelShaderFunction (); 

Ne vous inquiétez pas si vous n'êtes pas familier avec HLSL. Voyons comment cela fonctionne.

 échantillonneur TextureSampler: registre (s0);

Cette première partie déclare un échantillonneur de texture appelé TextureSampler. SpriteBatch liera une texture à cet échantillonneur lorsqu’il dessine avec ce shader. La spécification du registre auquel se lier est facultative. Nous utilisons l'échantillonneur pour rechercher des pixels à partir de la texture liée.

 float BloomThreshold;

BloomThreshold est un paramètre que nous pouvons définir à partir de notre code C #.

 float4 PixelShaderFunction (float2 texCoord: TEXCOORD0): COLOR0 

C'est notre déclaration de fonction pixel shader qui prend les coordonnées de texture en entrée et retourne une couleur. La couleur est retournée en tant que float4. Ceci est une collection de quatre chars, un peu comme un Vecteur4 dans XNA. Ils stockent les composantes rouge, verte, bleue et alpha de la couleur sous forme de valeurs comprises entre zéro et un..

TEXCOORD0 et COLOR0 sont appelés sémantique, et ils indiquent au compilateur comment le texCoord paramètre et la valeur de retour sont utilisés. Pour chaque sortie de pixel, texCoord contiendra les coordonnées du point correspondant dans la texture en entrée, avec (0, 0) étant le coin en haut à gauche et (1, 1) être en bas à droite.

 // Recherche la couleur de l'image d'origine. float4 c = tex2D (TextureSampler, texCoord); // Ajustez-le pour ne garder que les valeurs plus claires que le seuil spécifié. return saturate ((c - BloomThreshold) / (1 - BloomThreshold));

C'est là que tout le vrai travail est fait. Il récupère la couleur de pixel de la texture, soustrait BloomThreshold à partir de chaque composante de couleur, puis la redimensionne pour que la valeur maximale soit un. le saturer() la fonction serre ensuite les composants de la couleur entre zéro et un.

Vous remarquerez peut-être que c et BloomThreshold ne sont pas du même type que c est un float4 et BloomThreshold est un flotte. HLSL vous permet de faire des opérations avec ces différents types en tournant essentiellement le flotte dans une float4 avec tous les composants les mêmes. (c - BloomThreshold) devient effectivement:

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

Le reste du shader crée simplement une technique qui utilise la fonction pixel shader, compilée pour le shader modèle 2.0..

le Flou gaussien Shader

Un flou gaussien rend une image floue au moyen d’une fonction gaussienne. Pour chaque pixel de l'image de sortie, nous additionnons les pixels de l'image d'entrée pondérés par leur distance par rapport au pixel cible. Les pixels proches contribuent grandement à la couleur finale tandis que les pixels distants contribuent très peu.

Comme les pixels distants apportent une contribution négligeable et que les recherches de texture sont coûteuses, nous échantillonnons uniquement les pixels situés dans un rayon court au lieu d’échantillonner la texture entière. Ce shader échantillonnera des points situés à moins de 14 pixels du pixel actuel..

Une implémentation naïve peut échantillonner tous les points d'un carré autour du pixel actuel. Cependant, cela peut être coûteux. Dans notre exemple, nous devrions échantillonner des points dans un carré de 29 x 29 (14 points de chaque côté du pixel central, plus le pixel central). Cela représente un total de 841 échantillons pour chaque pixel de notre image. Heureusement, il existe une méthode plus rapide. Il s'avère que faire un flou gaussien en 2D équivaut à rendre d'abord flou l'image horizontalement, puis à nouveau flou verticalement. Chacun de ces flous unidimensionnels ne nécessite que 29 échantillons, ce qui réduit notre total à 58 échantillons par pixel..

Une autre astuce est utilisée pour augmenter encore l’efficacité du flou. Lorsque vous indiquez au GPU d’échantillonner entre deux pixels, le résultat obtenu est un mélange des deux pixels sans coût de performance supplémentaire. Comme notre flou mélange malgré tout les pixels, cela nous permet d’échantillonner deux pixels à la fois. Cela réduit presque de moitié le nombre d'échantillons requis.

Vous trouverez ci-dessous les parties pertinentes du Flou gaussien shader.

 échantillonneur TextureSampler: registre (s0); #define SAMPLE_COUNT 15 float2 SampleOffsets [SAMPLE_COUNT]; float SampleWeights [SAMPLE_COUNT]; float4 PixelShaderFunction (float2 texCoord: TEXCOORD0): COLOR0 float4 c = 0; // Combine un nombre de prises pondérées de filtres d'image. pour (int i = 0; i < SAMPLE_COUNT; i++)  c += tex2D(TextureSampler, texCoord + SampleOffsets[i]) * SampleWeights[i];  return c; 

Le shader est en réalité assez simple. il faut juste un tableau de compensations et un tableau correspondant de pondérations et calcule la somme pondérée. Toutes les mathématiques complexes se trouvent en réalité dans le code C # qui renseigne les tableaux de décalage et de pondération. Ceci est fait dans le SetBlurEffectParameters () et ComputeGaussian () méthodes de la BloomComponent classe. Lors de l'exécution de la passe de flou horizontal, SampleOffsets sera peuplé avec seulement des décalages horizontaux (les composantes y sont toutes égales à zéro), et bien sûr l'inverse est vrai pour la passe verticale.

le BloomCombine Shader

le BloomCombine shader fait plusieurs choses à la fois. Il combine la texture de floraison avec la texture d'origine tout en ajustant l'intensité et la saturation de chaque texture.

Le shader commence par déclarer deux échantillonneurs de texture et quatre paramètres float..

 échantillonneur BloomSampler: registre (s0); échantillonneur BaseSampler: registre (s1); float BloomIntensity; float BaseIntensity; float BloomSaturation; float BaseSaturation;

Une chose à noter est que SpriteBatch liera automatiquement la texture que vous passez lorsque vous appelez SpriteBatch.Draw () premier échantillonneur, mais il ne liera automatiquement rien au deuxième échantillonneur. Le deuxième échantillonneur est réglé manuellement dans BloomComponent.Draw () avec la ligne suivante.

 GraphicsDevice.Textures [1] = sceneRenderTarget;

Ensuite, nous avons une fonction d'assistance qui ajuste la saturation d'une couleur.

 float4 AdjustSaturation (couleur float4, saturation float) // Les constantes 0,3, 0,59 et 0,11 sont choisies car l'œil humain // est plus sensible à la lumière verte et moins au bleu. float grey = point (couleur, float3 (0,3, 0,59, 0,11)); retourne lerp (gris, couleur, saturation); 

Cette fonction prend une couleur et une valeur de saturation et renvoie une nouvelle couleur. En passant une saturation de 1 laisse la couleur inchangée. Qui passe 0 retournera le gris et les valeurs supérieures à un renverront une couleur avec une saturation accrue. Passer des valeurs négatives est vraiment en dehors de l'utilisation prévue, mais inversera la couleur si vous le faites.

La fonction fonctionne en recherchant d'abord la luminosité de la couleur en prenant une somme pondérée en fonction de la sensibilité de nos yeux à la lumière rouge, verte et bleue. Il interpole ensuite linéairement entre le gris et la couleur d'origine en fonction de la saturation spécifiée. Cette fonction est appelée par la fonction pixel shader.

 float4 PixelShaderFunction (float2 texCoord: TEXCOORD0): COLOR0 // Recherche les couleurs de la floraison et de l'image de base d'origine. float4 bloom = tex2D (BloomSampler, texCoord); float4 base = tex2D (BaseSampler, texCoord); // Ajuste la saturation et l'intensité des couleurs. bloom = AdjustSaturation (bloom, BloomSaturation) * BloomIntensity; base = AdjustSaturation (base, BaseSaturation) * BaseIntensity; // Assombrit l'image de base dans les zones très fleuries, // pour éviter que les éléments ne paraissent excessivement brûlés. base * = (1 - saturé (floraison)); // Combine les deux images. retour base + floraison; 

Encore une fois, ce shader est assez simple. Si vous vous demandez pourquoi l’image de base doit être assombrie dans les zones à floraison brillante, n’oubliez pas qu’ajouter deux couleurs ensemble augmente la luminosité et que toute composante de couleur dont la valeur est supérieure à un (luminosité maximale) sera coupée en une seule. . Etant donné que l'image de fond est similaire à l'image de base, la plupart des images dont la luminosité est supérieure à 50% sont utilisées au maximum. L’obscurcissement de l’image de base mappe toutes les couleurs dans la plage de couleurs que nous pouvons afficher correctement..


Trous noirs

L'un des ennemis les plus intéressants de Geometry Wars est le trou noir. Examinons comment nous pouvons créer quelque chose de similaire dans Shape Blaster. Nous allons créer la fonctionnalité de base maintenant, et nous reverrons l'ennemi dans le prochain tutoriel pour ajouter des effets et des interactions de particules..

Un trou noir avec des particules en orbite

Fonctionnalité de base

Les trous noirs attireront le vaisseau du joueur, les ennemis proches et (après le prochain tutoriel) des particules, mais repousseront les balles..

Il existe de nombreuses fonctions que nous pouvons utiliser pour attirer ou repousser. Le plus simple consiste à utiliser une force constante afin que le trou noir tire avec la même force, quelle que soit la distance de l'objet. Une autre option consiste à augmenter la force de façon linéaire, de zéro à une distance maximale, à la force maximale pour les objets directement au-dessus du trou noir..

Si nous souhaitons modéliser la gravité de manière plus réaliste, nous pouvons utiliser le carré inverse de la distance, ce qui signifie que la force de gravité est proportionnelle à \ (1 / distance ^ 2 \). Nous utiliserons chacune de ces trois fonctions pour gérer différents objets. Les balles seront repoussées avec une force constante, les ennemis et le vaisseau du joueur seront attirés par une force linéaire, et les particules utiliseront une fonction carrée inverse.

Nous allons faire une nouvelle classe pour les trous noirs. Commençons par les fonctionnalités de base.

 class BlackHole: Entity private statique Random rand = new Random (); privé int points de repère = 10; BlackHole public (position Vector2) image = Art.BlackHole; Position = position; Rayon = image.Width / 2f;  public void WasShot () hitpoints--; si (points de repère <= 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);  

Les trous noirs prennent dix coups à tuer. Nous ajustons légèrement l'échelle de l'image-objet pour la faire vibrer. Si vous décidez que la destruction de trous noirs doit également vous rapporter des points, vous devez procéder aux mêmes ajustements. Trou noir classe comme nous l'avons fait avec la classe ennemie.

Nous allons ensuite faire en sorte que les trous noirs appliquent une force sur d'autres entités. Nous aurons besoin d'une petite méthode d'assistance de notre EntityManager.

 public statique IEnumerable GetNearbyEntities (position Vector2, rayon de flottant) entités de retour. Où (x => Vector2.DistanceSquared (position, x.Position) < radius * radius); 

Cette méthode pourrait être rendue plus efficace en utilisant un schéma de partitionnement spatial plus compliqué, mais pour le nombre d'entités que nous aurons, tout va bien. Maintenant, nous pouvons faire en sorte que les trous noirs appliquent une force Mettre à jour() méthode.

 remplacement public void Update () var entités = EntityManager.GetNearbyEntities (Position, 250); foreach (entité var dans les entités) si (l'entité est Enemy &&! (l'entité est Enemy) .IsActive) continue; // les balles sont repoussées par des trous noirs et tout le reste est attiré si (entity est Bullet) entity.Velocity + = (entity.Position - Position) .ScaleTo (0.3f); else var dPos = Position - entity.Position; var longueur = dPos.Length (); entity.Velocity + = dPos.ScaleTo (MathHelper.Lerp (2, 0, length / 250f)); 

Les trous noirs n'affectent que les entités situées dans un rayon choisi (250 pixels). Les balles situées dans ce rayon ont une force de répulsion constante, tandis que tout le reste a une force d'attraction linéaire..

Nous devrons ajouter la gestion des collisions pour les trous noirs à la EntityManager. Ajouter un Liste <> pour les trous noirs comme nous l'avons fait pour les autres types d'entités, et ajoutez le code suivant dans EntityManager.HandleCollisions ().

 // gérer les collisions avec des trous noirs pour (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;  

Enfin, ouvrez le EnemySpawner classe et qu’il crée des trous noirs. J'ai limité le nombre maximum de trous noirs à deux et j'ai donné une chance sur 600 qu'un trou noir apparaisse à chaque image..

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

Conclusion

Nous avons ajouté bloom à l'aide de différents shaders et trous noirs à l'aide de différentes formules de force. Shape Blaster commence à bien paraître. Dans la partie suivante, nous ajouterons des effets de particule fous sur le dessus.