Crea un disparador de vectores de neón en XNA Bloom and Black Holes

En esta serie de tutoriales, te mostraré cómo hacer un juego de disparos de neón, como Geometry Wars, en XNA. El objetivo de estos tutoriales no es dejarte con una réplica exacta de Geometry Wars, sino repasar los elementos necesarios que te permitirán crear tu propia variante de alta calidad..


Visión general

En la serie hasta ahora, hemos configurado el modo de juego básico para nuestro juego de disparos de neón con doble varilla, Shape Blaster. En este tutorial crearemos la apariencia de neón de la firma agregando un filtro de post-procesamiento de bloom.

Advertencia: Alto!

Los efectos simples como este o efectos de partículas pueden hacer que un juego sea mucho más atractivo sin necesidad de cambios en el juego. El uso efectivo de los efectos visuales es una consideración importante en cualquier juego. Después de agregar el filtro de floración, también agregaremos agujeros negros al juego..


Efecto de posprocesamiento de la floración

Bloom describe el efecto que ves cuando miras un objeto con una luz brillante detrás y la luz parece sangrar sobre el objeto. En Shape Blaster, el efecto de floración hará que las líneas brillantes de las naves y las partículas se vean como luces de neón brillantes y brillantes..

La luz del sol florece a través de los árboles

Para aplicar la floración en nuestro juego, debemos renderizar nuestra escena a un objetivo de renderizado, y luego aplicar nuestro filtro de floración a ese objetivo de renderizado..

Bloom trabaja en tres pasos:

  1. Extraer las partes brillantes de la imagen..
  2. Difuminar las partes brillantes.
  3. Vuelva a combinar la imagen borrosa con la imagen original mientras realiza algunos ajustes de brillo y saturación.

Cada uno de estos pasos requiere una sombreador - Esencialmente un programa corto que se ejecuta en su tarjeta gráfica. Los Shaders en XNA están escritos en un lenguaje especial llamado High-Level Shader Language (HLSL). Las imágenes de muestra a continuación muestran el resultado de cada paso..

Imagen inicial Las áreas brillantes extraídas de la imagen. Las áreas brillantes después de difuminar El resultado final tras la recombinación con la imagen original.

Añadiendo Bloom a Shape Blaster

Para nuestro filtro de floración, utilizaremos la muestra de postproceso Bloom de XNA.

Integrar la muestra de floración con nuestro proyecto es fácil. Primero, localice los dos archivos de código de la muestra, BloomComponent.cs y BloomSettings.cs, y agregarlos a la ShapeBlaster proyecto. También agrega BloomCombine.fx, BloomExtract.fx, y GaussianBlur.fx al proyecto de canalización de contenido.

En GameRoot, Agrega un utilizando declaración para el BloomPostproceso espacio de nombres y añadir una BloomComponent variable miembro.

 BloomComponent bloom;

En el GameRoot constructor, añade las siguientes lineas.

 bloom = nuevo BloomComponent (este); Componentes.Agrega (floración); bloom.Settings = new BloomSettings (null, 0.25f, 4, 2, 1, 1.5f, 1);

Finalmente, al principio de GameRoot.Draw (), agrega la siguiente linea.

 bloom.BeginDraw ();

Eso es. Si ejecutas el juego ahora, deberías ver la floración en efecto.

Cuando usted llama bloom.BeginDraw (), redirige las llamadas de sorteo subsiguientes a un destino de renderizado al que se aplicará la floración. Cuando usted llama base.Draw () al final de GameRoot.Draw () método, el BloomComponentes Dibujar() se llama metodo Aquí es donde se aplica la floración y la escena se dibuja en el búfer posterior. Por lo tanto, cualquier cosa que necesite que se aplique la floración debe dibujarse entre las llamadas a bloom.BeginDraw () y base.Draw ().

Propina: Si desea dibujar algo sin floración (por ejemplo, la interfaz de usuario), dibuje después la llamada a base.Draw ().

Puede ajustar la configuración de la floración a su gusto. He elegido los siguientes valores:

  • 0.25 para el umbral de la floración. Esto significa que cualquier parte de la imagen que tenga menos de un cuarto de brillo total no contribuirá a la floración.
  • 4 por la cantidad de desenfoque. Para los inclinados matemáticamente, esta es la desviación estándar del desenfoque gaussiano. Los valores más grandes harán que la luz florezca más. Sin embargo, tenga en cuenta que el sombreado de desenfoque está configurado para utilizar un número fijo de muestras, independientemente de la cantidad de desenfoque. Si establece este valor demasiado alto, el desenfoque se extenderá más allá del radio desde el cual aparecerán las muestras del sombreado y los artefactos. Idealmente, este valor no debe ser más de un tercio de su radio de muestreo para garantizar que el error sea insignificante.
  • 2 para la intensidad de la floración, que determina con qué intensidad afecta la floración al resultado final.
  • 1 para la intensidad de la base, que determina la intensidad con la que la imagen original afecta el resultado final.
  • 1.5 Para la saturación de la floración. Esto hace que el brillo alrededor de los objetos brillantes tenga colores más saturados que los objetos mismos. Se eligió un valor alto para simular el aspecto de las luces de neón. Si miras el centro de una luz de neón brillante, se ve casi blanca, mientras que el brillo a su alrededor es de color más intenso..
  • 1 para la saturación de bases. Este valor afecta a la saturación de la imagen base..
Sin flor Con flor

Bloom bajo el capó

El filtro de floración se implementa en el BloomComponent clase. El componente bloom comienza creando y cargando los recursos necesarios en su LoadContent () método. Aquí, carga los tres sombreadores que requiere y crea tres objetivos de renderizado..

El primer objetivo de render, sceneRenderTarget, es para sostener la escena a la que se aplicará la floración. Los otros dos, renderTarget1 y renderTarget2, se utilizan para mantener temporalmente los resultados intermedios entre cada paso de representación. Estos objetivos de renderización se hacen a la mitad de la resolución del juego para reducir el costo de rendimiento. Esto no reduce la calidad final de la floración, porque de todos modos estaremos borrando las imágenes de la floración.

Bloom requiere cuatro pases de representación, como se muestra en este diagrama:

En XNA, la Efecto clase encapsula un shader. Usted escribe el código para el shader en un archivo separado, que agrega al canal de contenido. Estos son los archivos con el .fx extensión que hemos añadido anteriormente. Cargas el shader en un Efecto objeto llamando al Contenido.Cargar() método en LoadContent (). La forma más fácil de usar un sombreador en un juego 2D es pasar el Efecto objeto como un parámetro para SpriteBatch.Begin ().

Hay varios tipos de sombreadores, pero para el filtro de floración solo usaremos sombreadores de píxeles (aveces llamado sombreadores de fragmentos). Un sombreador de píxeles es un pequeño programa que se ejecuta una vez por cada píxel que dibuja y determina el color del píxel. Repasaremos cada uno de los shaders utilizados..

los BloomExtract Shader

los BloomExtract El shader es el más simple de los tres shaders. Su trabajo consiste en extraer las áreas de la imagen que son más brillantes que algún umbral y luego volver a escalar los valores de color para utilizar el rango de color completo. Cualquier valor por debajo del umbral se volverá negro..

El código completo del sombreador se muestra a continuación.

 muestreador TextureSampler: registro (s0); flotar BloomThreshold; float4 PixelShaderFunction (float2 texCoord: TEXCOORD0): COLOR0 // Busque el color de la imagen original. float4 c = tex2D (TextureSampler, texCoord); // Ajústelo para mantener solo los valores más brillantes que el umbral especificado. retorno saturado ((c - BloomThreshold) / (1 - BloomThreshold));  técnica BloomExtract pasar Pass1 PixelShader = compilar ps_2_0 PixelShaderFunction (); 

No se preocupe si no está familiarizado con HLSL. Vamos a examinar cómo funciona esto.

 muestreador TextureSampler: registro (s0);

Esta primera parte declara una muestra de texturas llamada TexturaSampler. SpriteBatch enlazará una textura a esta muestra cuando dibuje con este sombreador. Especificar a qué registro vincular es opcional. Usamos la muestra para buscar píxeles de la textura encuadernada.

 flotar BloomThreshold;

BloomThreshold es un parámetro que podemos configurar desde nuestro código C #.

 float4 PixelShaderFunction (float2 texCoord: TEXCOORD0): COLOR0 

Esta es nuestra declaración de función de sombreado de píxeles que toma las coordenadas de la textura como entrada y devuelve un color. El color se devuelve como un flotar4. Esta es una colección de cuatro carrozas, muy parecida a una Vector4 en XNA. Almacenan los componentes rojo, verde, azul y alfa del color como valores entre cero y uno..

TEXCOORD0 y COLOR0 son llamados semántica, y le indican al compilador como el texCoord Se utilizan el parámetro y el valor de retorno. Para cada salida de píxel, texCoord contendrá las coordenadas del punto correspondiente en la textura de entrada, con (0, 0) siendo la esquina superior izquierda y (1, 1) siendo la parte inferior derecha.

 // Busca el color de la imagen original. float4 c = tex2D (TextureSampler, texCoord); // Ajústelo para mantener solo los valores más brillantes que el umbral especificado. retorno saturado ((c - BloomThreshold) / (1 - BloomThreshold));

Aquí es donde se realiza todo el trabajo real. Obtiene el color del píxel de la textura, resta. BloomThreshold de cada componente de color, y luego vuelve a escalarlo para que el valor máximo sea uno. los saturar() La función luego sujeta los componentes del color entre cero y uno..

Usted puede notar que do y BloomThreshold no son del mismo tipo, como do es un flotar4 y BloomThreshold es un flotador. HLSL le permite realizar operaciones con estos diferentes tipos esencialmente girando el flotador en una flotar4 con todos los componentes iguales. (c - BloomThreshold) efectivamente se convierte en:

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

El resto del sombreado simplemente crea una técnica que utiliza la función de sombreado de píxeles, compilada para el modelo de sombreado 2.0..

los Desenfoque gaussiano Shader

Un desenfoque gaussiano desenfoca una imagen usando una función gaussiana. Para cada píxel en la imagen de salida, resumimos los píxeles en la imagen de entrada ponderados por su distancia desde el píxel objetivo. Los píxeles cercanos contribuyen en gran medida al color final, mientras que los píxeles distantes contribuyen muy poco.

Debido a que los píxeles distantes hacen contribuciones insignificantes y las búsquedas de texturas son costosas, solo muestreamos píxeles en un radio corto en lugar de muestrear toda la textura. Este sombreador muestreará puntos dentro de los 14 píxeles del píxel actual.

Una implementación ingenua podría muestrear todos los puntos en un cuadrado alrededor del píxel actual. Sin embargo, esto puede ser costoso. En nuestro ejemplo, tendríamos que muestrear puntos dentro de un cuadrado de 29x29 (14 puntos a cada lado del píxel central, más el píxel central). Eso es un total de 841 muestras por cada píxel en nuestra imagen. Por suerte, hay un método más rápido. Resulta que hacer un desenfoque gaussiano 2D es equivalente a desenfocar primero la imagen horizontalmente, y luego desenfocarla de nuevo verticalmente. Cada uno de estos borrones unidimensionales solo requiere 29 muestras, lo que reduce nuestro total a 58 muestras por píxel.

Se utiliza un truco más para aumentar aún más la eficiencia del desenfoque. Cuando le dice a la GPU que muestree entre dos píxeles, devolverá una combinación de los dos píxeles sin costo de rendimiento adicional. Como nuestra imagen borrosa está combinando píxeles de todos modos, esto nos permite muestrear dos píxeles a la vez. Esto reduce el número de muestras requeridas casi a la mitad.

A continuación se presentan las partes relevantes de la Desenfoque gaussiano sombreador.

 muestreador TextureSampler: registro (s0); #define SAMPLE_COUNT 15 float2 SampleOffsets [SAMPLE_COUNT]; float SampleWeights [SAMPLE_COUNT]; float4 PixelShaderFunction (float2 texCoord: TEXCOORD0): COLOR0 float4 c = 0; // Combinar un número de tomas de filtro de imagen ponderada. para (int i = 0; i < SAMPLE_COUNT; i++)  c += tex2D(TextureSampler, texCoord + SampleOffsets[i]) * SampleWeights[i];  return c; 

El shader es en realidad bastante simple; solo toma una matriz de compensaciones y una matriz correspondiente de pesos y calcula la suma ponderada. Todas las matemáticas complejas están en realidad en el código C # que rellena las matrices de desplazamiento y peso. Esto se hace en el SetBlurEffectParameters () y ComputeGaussian () métodos de la BloomComponent clase. Al realizar el pase borroso horizontal, SampleOffsets se rellenará solo con desplazamientos horizontales (los componentes y son todos cero) y, por supuesto, lo contrario es cierto para el paso vertical.

los BloomCombine Shader

los BloomCombine Shader hace algunas cosas a la vez. Combina la textura de la floración con la textura original al mismo tiempo que ajusta la intensidad y la saturación de cada textura..

El sombreador comienza declarando dos muestreadores de textura y cuatro parámetros de flotación.

 muestreador BloomSampler: registro (s0); muestreador BaseSampler: registro (s1); flotar Bloomintensidad; flotante BaseIntensidad; flotación BloomSaturation; flotación BaseSaturation;

Una cosa a tener en cuenta es que SpriteBatch Atará automáticamente la textura que pases cuando llames. SpriteBatch.Draw () a la primera muestra, pero no enlazará automáticamente nada a la segunda muestra. La segunda muestra se establece manualmente en BloomComponent.Draw () con la siguiente linea.

 GraphicsDevice.Textures [1] = sceneRenderTarget;

A continuación tenemos una función auxiliar que ajusta la saturación de un color..

 float4 AdjustSaturation (float4 color, float saturation) // Se seleccionan las constantes 0.3, 0.59 y 0.11 porque el ojo humano es más sensible a la luz verde y menos al azul. gris flotante = punto (color, float3 (0.3, 0.59, 0.11)); lerp de retorno (gris, color, saturación); 

Esta función toma un color y un valor de saturación y devuelve un nuevo color. Pasando una saturación de 1 deja el color sin cambios. Paso 0 devolverá el color gris y los valores pasados ​​mayores que uno devolverán un color con mayor saturación. Pasar valores negativos está realmente fuera del uso previsto, pero invertirá el color si lo haces.

La función funciona al encontrar primero la luminosidad del color tomando una suma ponderada basada en la sensibilidad de nuestros ojos a la luz roja, verde y azul. Luego se interpola linealmente entre el gris y el color original por la cantidad de saturación especificada. Esta función es llamada por la función de sombreado de píxeles..

 float4 PixelShaderFunction (float2 texCoord: TEXCOORD0): COLOR0 // Busque los colores de imagen base y originales. float4 bloom = tex2D (BloomSampler, texCoord); float4 base = tex2D (BaseSampler, texCoord); // Ajustar la saturación y la intensidad del color. bloom = Ajustar la saturación (bloom, BloomSaturation) * BloomIntensity; base = AdjustSaturation (base, BaseSaturation) * BaseIntensity; // Oscurece la imagen base en áreas donde hay mucha floración, // para evitar que las cosas se vean excesivamente quemadas. base * = (1 - saturar (flor)); // Combina las dos imágenes. base de retorno + floración; 

Una vez más, este shader es bastante sencillo. Si se está preguntando por qué la imagen base debe oscurecerse en áreas con floración brillante, recuerde que al agregar dos colores juntos aumenta el brillo y cualquier componente de color que se agregue a un valor mayor que uno (brillo completo) se recortará a uno . Dado que la imagen de la imagen es similar a la imagen base, esto causaría que gran parte de la imagen que tiene más del 50% de brillo se convierta en un máximo. El oscurecimiento de la imagen base asigna todos los colores al rango de colores que podemos mostrar correctamente.


Agujeros negros

Uno de los enemigos más interesantes en Geometry Wars es el agujero negro. Examinemos cómo podemos hacer algo similar en Shape Blaster. Crearemos la funcionalidad básica ahora, y volveremos a visitar al enemigo en el siguiente tutorial para agregar efectos de partículas e interacciones de partículas..

Un agujero negro con partículas en órbita.

Funcionalidad básica

Los agujeros negros tirarán de la nave del jugador, los enemigos cercanos y (después del siguiente tutorial) las partículas, pero repelerán las balas..

Hay muchas funciones posibles que podemos usar para la atracción o la repulsión. Lo más simple es usar una fuerza constante para que el agujero negro tire con la misma fuerza independientemente de la distancia del objeto. Otra opción es hacer que la fuerza aumente linealmente desde cero a cierta distancia máxima, hasta fuerza total para objetos directamente sobre el agujero negro..

Si quisiéramos modelar la gravedad de manera más realista, podemos usar el cuadrado inverso de la distancia, lo que significa que la fuerza de la gravedad es proporcional a \ (1 / distance ^ 2 \). En realidad, usaremos cada una de estas tres funciones para manejar diferentes objetos. Las balas serán repelidas con una fuerza constante, los enemigos y la nave del jugador serán atraídos con una fuerza lineal, y las partículas usarán una función de cuadrado inverso..

Haremos una nueva clase para agujeros negros. Empecemos con la funcionalidad básica..

 clase BlackHole: Entidad private static Random rand = new Random (); puntos de acceso int privados = 10; BlackHole público (posición Vector2) image = Art.BlackHole; Posición = posición; Radio = imagen.Ancho / 2f;  public void WasShot () hitpoints--; si <= 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);  

Los agujeros negros toman diez tiros para matar. Ajustamos la escala del sprite ligeramente para hacer que pulse. Si decide que la destrucción de los agujeros negros también debe otorgar puntos, debe realizar ajustes similares a los Agujero negro clase como lo hicimos con la clase enemiga.

A continuación haremos que los agujeros negros en realidad apliquen una fuerza sobre otras entidades. Necesitaremos un pequeño método de ayuda de nuestra EntityManager.

 Entidades públicas públicasEnumerables GetNearby (posición Vector2, radio de flotación) entidades de retorno.Donde (x => Vector2.DistanceSquared (posición, x.Posición) < radius * radius); 

Este método podría hacerse más eficiente utilizando un esquema de partición espacial más complicado, pero para la cantidad de entidades que tendremos, está bien tal como está. Ahora podemos hacer que los agujeros negros apliquen fuerza en sus Actualizar() método.

 Public Override void Update () var entities = EntityManager.GetNearbyEntities (Position, 250); foreach (var entidad en entidades) if (entidad es Enemigo &&! (entidad como Enemigo) .IsActive) continuar; // las balas son repelidas por los agujeros negros y todo lo demás es atraído si (la entidad es la Bala) entidad.Velocity + = (entidad.Posición - Posición). Escala A (0.3f); else var dPos = Position - entity.Position; longitud var = dPos.Length (); entity.Velocity + = dPos.ScaleTo (MathHelper.Lerp (2, 0, length / 250f)); 

Los agujeros negros solo afectan a las entidades dentro de un radio elegido (250 píxeles). Las balas dentro de este radio tienen una fuerza de repulsión constante aplicada, mientras que todo lo demás tiene una fuerza atractiva lineal aplicada.

Tendremos que agregar manejo de colisiones para agujeros negros a la EntityManager. Agrega un Lista <> para los agujeros negros, como hicimos con los otros tipos de entidades, y agregue el siguiente código en EntityManager.HandleCollisions ().

 // manejar colisiones con agujeros negros para (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;  

Finalmente, abre el EnemySpawner Clase y haz que creen algunos agujeros negros. Limité el número máximo de agujeros negros a dos, y di una probabilidad de 1 en 600 de que un agujero negro genere cada cuadro.

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

Conclusión

Hemos agregado la floración con varios sombreadores y los agujeros negros con varias fórmulas de fuerza. Shape Blaster está empezando a verse bastante bien. En la siguiente parte, agregaremos un poco de locura, sobre los efectos de partículas superiores.