From 7f7a21c1100956bf346f63dd3cb50e05d79a0796 Mon Sep 17 00:00:00 2001 From: Michel Fedde Date: Sun, 14 May 2023 19:54:50 +0200 Subject: [PATCH] Added explosion --- CityGame/CityGame.csproj | 10 ++ .../Classes/Rendering/Particles/Explosion.cs | 131 ++++++++++++++++++ .../Rendering/Particles/ExplosionParticle.cs | 21 +++ .../Classes/Rendering/Particles/explosion.cfx | Bin 0 -> 8688 bytes .../Classes/Rendering/Particles/explosion.fx | 121 ++++++++++++++++ .../Classes/Rendering/Shaders/ShaderLoader.cs | 58 ++++++++ CityGame/MainWindow.cs | 4 + CityGame/Resources/NoiseTexture.png | Bin 0 -> 7020 bytes 8 files changed, 345 insertions(+) create mode 100644 CityGame/Classes/Rendering/Particles/Explosion.cs create mode 100644 CityGame/Classes/Rendering/Particles/ExplosionParticle.cs create mode 100644 CityGame/Classes/Rendering/Particles/explosion.cfx create mode 100644 CityGame/Classes/Rendering/Particles/explosion.fx create mode 100644 CityGame/Classes/Rendering/Shaders/ShaderLoader.cs create mode 100644 CityGame/Resources/NoiseTexture.png diff --git a/CityGame/CityGame.csproj b/CityGame/CityGame.csproj index 7fedb76..ec10931 100644 --- a/CityGame/CityGame.csproj +++ b/CityGame/CityGame.csproj @@ -11,12 +11,18 @@ + + + + Always + + @@ -162,4 +168,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 0000000000000000000000000000000000000000..63b6f12ac03369fca6b5f53adc4fb726d49502b4 GIT binary patch literal 8688 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 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 70e5ecf..c70e62a 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; @@ -160,6 +162,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 0000000000000000000000000000000000000000..6cdd16eec48476270bd2831ec43cadfa94fd0e07 GIT binary patch literal 7020 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 KMNUMnLSTXtZx+4) literal 0 HcmV?d00001