diff --git a/CityGame/CityGame.csproj b/CityGame/CityGame.csproj
index b6308b8..a921639 100644
--- a/CityGame/CityGame.csproj
+++ b/CityGame/CityGame.csproj
@@ -11,12 +11,18 @@
+
+
+
+ Always
+
+
@@ -163,4 +169,8 @@
+
+
+
+
diff --git a/CityGame/Classes/Rendering/Particles/Explosion.cs b/CityGame/Classes/Rendering/Particles/Explosion.cs
new file mode 100644
index 0000000..a9493d0
--- /dev/null
+++ b/CityGame/Classes/Rendering/Particles/Explosion.cs
@@ -0,0 +1,131 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using CityGame.Classes.Rendering.Shaders;
+using CityGame.Classes.World;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using OrpticonGameHelper;
+using OrpticonGameHelper.Classes.Particles;
+using OrpticonGameHelper.Classes.Utils;
+using static CityGame.MainWindow;
+
+namespace CityGame.Classes.Rendering.Particles;
+
+public sealed class Explosion : ParticleEmitter, IParticleHandler
+{
+ internal const string SHADER_PATH = "CityGame.Classes.Rendering.Particles.explosion.cfx";
+ private const float PIXEL_TO_TILE_RATIO = 1 / 2f;
+
+ private static Effect? _explosionShader = null;
+
+ public Color Color = Color.Yellow;
+
+ public Explosion()
+ : base(Explosion.GetShader())
+ {
+ this.ParticleHandler = this;
+ }
+
+ private static Effect GetShader()
+ {
+ if (Explosion._explosionShader == null)
+ {
+ Explosion._explosionShader = ShaderLoader.Get(Explosion.SHADER_PATH);
+ }
+
+ return Explosion._explosionShader;
+ }
+
+ public ICollection Generate()
+ {
+ List particles = new List();
+ for (int i = 0; i < 8; i++)
+ {
+ ExplosionParticle particle = new ExplosionParticle();
+ particle.OriginalPosition = particle.Position = Vector2.Zero;
+
+ Vector2 direction = new Vector2(Random.Shared.NextSingle(-1, 1), Random.Shared.NextSingle(-1,1));
+ particle.Direction = direction;
+
+ particle.LifeTime = EmissionTime;
+ particle.LifeTimeScale = Random.Shared.NextSingle(1, 1.5f);
+
+ particle.UVOffset = new Vector2(Random.Shared.NextSingle(1), Random.Shared.NextSingle(1));
+
+ particles.Add(particle);
+ }
+
+ return particles;
+ }
+
+ public void Move(ICollection particles, float deltatime, ParticleEmitter emitter)
+ {
+
+ foreach (IParticle particle in particles)
+ {
+ float time = emitter.NormalizedTime;
+ if (particle is ExplosionParticle explParticle)
+ {
+ explParticle.ReduceLifeTime(deltatime);
+ time = ParticleEmitter.CalculateNormalizedReversedTime(explParticle.LifeTime, this.EmissionTime);
+ }
+
+ float distanceCalc = Easing.Easing.OutExpo(time);
+ float distance = distanceCalc * emitter.MaxParticleDistance;
+ particle.Position = particle.OriginalPosition + particle.Direction * distance;
+
+ }
+ }
+
+ public void Draw(ref ParticleDrawContext context)
+ {
+ context.Effect.Parameters["view_projection"].SetValue(context.View * context.Projection);
+ context.Effect.Parameters["color"].SetValue(this.Color.ToVector3());
+
+
+
+ context.SpriteBatch.Begin(
+ effect: Effect,
+ blendState: BlendState.NonPremultiplied,
+ sortMode: SpriteSortMode.Immediate,
+ samplerState: SamplerState.LinearWrap
+ );
+
+ float pixelSize = MainWindow.TileSize * Explosion.PIXEL_TO_TILE_RATIO;
+ float sizeToTileRatio = (float)context.Size / MainWindow.TileSize;
+ int resultingPixelSize = (int)Math.Ceiling(pixelSize * sizeToTileRatio);
+
+ context.Effect.Parameters["size"].SetValue(context.Size / 2);
+ context.Effect.Parameters["pixelSize"].SetValue(resultingPixelSize / 2);
+ foreach (IParticle particle in context.Particles)
+ {
+ if (!particle.IsActive)
+ {
+ continue;
+ }
+
+ float normTime = NormalizedTime;
+ if (particle is ExplosionParticle explParticle)
+ {
+ normTime = ParticleEmitter.CalculateNormalizedReversedTime(explParticle.LifeTime, this.EmissionTime);
+ context.Effect.Parameters["noiseOffset"].SetValue(explParticle.UVOffset);
+ }
+ float time = Easing.Easing.OutExpo(normTime);
+
+ context.Effect.Parameters["timing"].SetValue(time);
+ context.Effect.Parameters["position"].SetValue(context.EmitterCenterPosition + particle.Position);
+ int x = (int)(context.EmitterRenderPosition.X + particle.Position.X);
+ int y = (int)(context.EmitterRenderPosition.Y + particle.Position.Y);
+
+ context.SpriteBatch.Draw(
+ Window._textures[Environment.CurrentDirectory + "\\Resources\\NoiseTexture.png"],
+ new Rectangle(x, y, context.Size, context.Size),
+ Color.White
+ );
+ }
+
+ context.SpriteBatch.End();
+ }
+}
\ No newline at end of file
diff --git a/CityGame/Classes/Rendering/Particles/ExplosionParticle.cs b/CityGame/Classes/Rendering/Particles/ExplosionParticle.cs
new file mode 100644
index 0000000..391cb2f
--- /dev/null
+++ b/CityGame/Classes/Rendering/Particles/ExplosionParticle.cs
@@ -0,0 +1,21 @@
+using Microsoft.Xna.Framework;
+using OrpticonGameHelper.Classes.Particles;
+
+namespace CityGame.Classes.Rendering.Particles;
+
+public sealed class ExplosionParticle: Particle
+{
+ public float LifeTimeScale = 1;
+ public float LifeTime = 1;
+ public Vector2 UVOffset = Vector2.Zero;
+
+ public void ReduceLifeTime(float deltatime)
+ {
+ this.LifeTime -= deltatime * this.LifeTimeScale;
+
+ if (this.LifeTime <= 0)
+ {
+ this.IsActive = false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/CityGame/Classes/Rendering/Particles/explosion.cfx b/CityGame/Classes/Rendering/Particles/explosion.cfx
new file mode 100644
index 0000000..63b6f12
Binary files /dev/null and b/CityGame/Classes/Rendering/Particles/explosion.cfx differ
diff --git a/CityGame/Classes/Rendering/Particles/explosion.fx b/CityGame/Classes/Rendering/Particles/explosion.fx
new file mode 100644
index 0000000..7774a2a
--- /dev/null
+++ b/CityGame/Classes/Rendering/Particles/explosion.fx
@@ -0,0 +1,121 @@
+#if OPENGL
+#define VS_SHADERMODEL vs_3_0
+#define PS_SHADERMODEL ps_3_0
+#else
+#define VS_SHADERMODEL vs_4_0_level_9_1
+#define PS_SHADERMODEL ps_4_0_level_9_1
+#endif
+
+sampler TextureSampler : register(s0);
+
+float4x4 view_projection;
+float2 position;
+int size;
+int pixelSize;
+float timing;
+float2 noiseOffset;
+
+float3 color;
+
+#define BLUR_KERNEL_X_MIN -0.5
+#define BLUR_KERNEL_X_MAX 0.5
+#define BLUR_KERNEL_X_JUMP 0.5
+#define BLUR_KERNEL_Y_MIN -0.5
+#define BLUR_KERNEL_Y_MAX 0.5
+#define BLUR_KERNEL_Y_JUMP 0.5
+
+struct VertexInput {
+ float4 Position : POSITION0;
+ float2 TexCoord : TEXCOORD0;
+};
+struct PixelInput {
+ float4 Position : SV_Position0;
+ float2 RelativePos : TEXCOORD0;
+ float2 NoiseTextureCoord : TEXCOORD1;
+};
+
+float2 pixelizeCoord(float2 coord)
+{
+ float2 uv = coord / size;
+ float2 scaledUV = uv * pixelSize;
+
+ return scaledUV;
+}
+float2 scalePixelCoordBack(float2 coord)
+{
+ return round(coord) / pixelSize;
+}
+
+PixelInput SpriteVertexShader(VertexInput v) {
+ PixelInput output;
+
+ output.RelativePos = pixelizeCoord(v.Position.xy - position);
+ output.Position = mul(v.Position, view_projection);
+ output.NoiseTextureCoord = (v.TexCoord + noiseOffset) * (pixelSize * 2);
+
+ return output;
+}
+float2 linear_mix(float t, float2 val1, float2 val2)
+{
+ float2 outval = val1;
+
+ if (val2[0] > 0.5)
+ outval[0] = val1[0] + t * (2.0 * (val2[0] - 0.5));
+ else
+ outval[0] = val1[0] + t * (2.0 * (val2[0]) - 1.0);
+
+ if (val2[1] > 0.5)
+ outval[1] = val1[1] + t * (2.0 * (val2[1] - 0.5));
+ else
+ outval[1] = val1[1] + t * (2.0 * (val2[1]) - 1.0);
+
+ return outval;
+}
+
+float2 getNoise(float2 uv)
+{
+ return tex2D(TextureSampler, uv).rg;
+}
+
+float calculateAlpha(float2 uv, float2 noiseUV)
+{
+ float2 actualUV = scalePixelCoordBack(uv);
+
+ float2 lengthUV = linear_mix(0.5, actualUV, getNoise(actualUV + noiseOffset));
+
+ return step(length(lengthUV), timing);
+}
+
+float4 SpritePixelShader(PixelInput p) : SV_TARGET {
+
+ float2 noiseCoord = round(p.NoiseTextureCoord) / (pixelSize * 2);
+ float2 relPos = abs(p.RelativePos);
+
+ int sampleAmount = 0;
+ float val = 0;
+ for(float x = BLUR_KERNEL_X_MIN; x <= BLUR_KERNEL_X_MAX; x += BLUR_KERNEL_X_JUMP)
+ {
+ for(float y = BLUR_KERNEL_X_MIN; y <= BLUR_KERNEL_X_MAX; y += BLUR_KERNEL_Y_JUMP)
+ {
+ float2 alphaUV = relPos + float2(x,y);
+ val += calculateAlpha(alphaUV, noiseCoord);
+ sampleAmount++;
+ }
+ }
+
+ val = val / sampleAmount;
+
+ val = val + calculateAlpha(relPos, noiseCoord);
+
+ float noiseResult = getNoise(noiseCoord).r;
+ float3 resultColor = color * lerp(0.5, 1, noiseResult);
+
+ return float4(resultColor,val);
+}
+
+technique SpriteBatch {
+ pass {
+ VertexShader = compile VS_SHADERMODEL SpriteVertexShader();
+ PixelShader = compile PS_SHADERMODEL SpritePixelShader();
+ }
+}
\ No newline at end of file
diff --git a/CityGame/Classes/Rendering/Shaders/ShaderLoader.cs b/CityGame/Classes/Rendering/Shaders/ShaderLoader.cs
new file mode 100644
index 0000000..e17b49d
--- /dev/null
+++ b/CityGame/Classes/Rendering/Shaders/ShaderLoader.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+using CityGame.Classes.Rendering.Particles;
+using Microsoft.Xna.Framework.Graphics;
+using OrpticonGameHelper;
+using OrpticonGameHelper.Classes;
+using OrpticonGameHelper.Classes.Utils;
+
+namespace CityGame.Classes.Rendering.Shaders;
+
+public sealed class ShaderLoader : ILoadable
+{
+ private static string[] _shaderPaths = new[]
+ {
+ Explosion.SHADER_PATH
+ };
+
+ private static ShaderLoader _instance;
+
+ private Dictionary _effects = new Dictionary();
+
+ private ShaderLoader() {}
+
+ public static ShaderLoader Create()
+ {
+ if (ShaderLoader._instance != null)
+ {
+ throw new Exception("There can only be one ShaderLoader.");
+ }
+
+ ShaderLoader._instance = new ShaderLoader();
+ return ShaderLoader._instance;
+ }
+
+ public void Load(Window window)
+ {
+
+ foreach (string shaderPath in ShaderLoader._shaderPaths)
+ {
+ byte[] shaderCode = AssemblyUtility.ReadAssemblyFileAsByte(shaderPath);
+
+ Effect shader = new Effect(window.GraphicsDevice, shaderCode);
+ this._effects[shaderPath] = shader;
+ }
+ }
+
+ public static Effect Get(string path)
+ {
+ if (!ShaderLoader._instance._effects.ContainsKey(path))
+ {
+ throw new Exception("No effect loaded with the path '" + path + "'.");
+ }
+
+ return ShaderLoader._instance._effects[path];
+ }
+}
\ No newline at end of file
diff --git a/CityGame/MainWindow.cs b/CityGame/MainWindow.cs
index 86da893..536375d 100644
--- a/CityGame/MainWindow.cs
+++ b/CityGame/MainWindow.cs
@@ -10,6 +10,8 @@ using SimplexNoise;
using System;
using System.Collections.Generic;
using System.Linq;
+using CityGame.Classes.Rendering.Particles;
+using CityGame.Classes.Rendering.Shaders;
using OrpticonGameHelper;
using OrpticonGameHelper.Classes.Elements;
using static CityGame.Classes.Entities.Car;
@@ -161,6 +163,8 @@ namespace CityGame
ImageConverter.ChangeColor("Car", "NPCCar", ColorConversionMaps.CarToNPCCar);
ImageConverter.ChangeColor("Car", "PoliceCar", ColorConversionMaps.CarToPoliceCar);
#endregion
+
+ LoadableContent.Add(ShaderLoader.Create());
int seed = 8;
diff --git a/CityGame/Resources/NoiseTexture.png b/CityGame/Resources/NoiseTexture.png
new file mode 100644
index 0000000..6cdd16e
Binary files /dev/null and b/CityGame/Resources/NoiseTexture.png differ