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