Add ParticleSystem, fix incorrect Argb conversion in Color, remove byte casting in LerpColor, update TestGame to demostrate particle system.

This commit is contained in:
2024-10-14 22:05:47 +02:00
parent e676e3d13d
commit a1d282908a
6 changed files with 247 additions and 216 deletions

View File

@@ -0,0 +1,205 @@
using System.Numerics;
using Voile.Rendering;
using Voile.Utils;
namespace Voile.Systems;
public struct Particle
{
public Particle()
{
}
public int EmitterIndex { get; set; }
public int ColorArgb { get; set; }
public Vector2 Position { get; set; }
public Vector2 Velocity { get; set; }
public float AngularVelocity { get; set; }
public float LifeTime { get; set; } = 1.0f;
public float LifeTimeRemaining { get; set; }
public float Scale { get; set; }
public float Rotation { get; set; }
public bool Alive { get; set; } = true;
}
public class ParticleEmitterSettings
{
public int MaxParticles { get; set; } = 16;
public float EmitRadius { get; set; }
public float LifeTime { get; set; } = 1.0f;
public float Explosiveness { get; set; }
public Vector2 Direction { get; set; } = -Vector2.UnitY;
public float LinearVelocity { get; set; } = 980.0f;
public float AngularVelocity { get; set; }
public float AngularVelocityRandom { get; set; }
public float LinearVelocityRandom { get; set; } = 0.5f;
public Vector2 Gravity { get; set; } = Vector2.UnitY * 980f;
public float ScaleBegin { get; set; } = 1.0f;
public float ScaleEnd { get; set; } = 0.0f;
public Color ColorBegin { get; set; } = Color.White;
public Color ColorEnd { get; set; } = Color.Black;
public float Damping { get; set; } = 1.0f;
}
public class ParticleEmitter : IUpdatableSystem
{
public ReadOnlySpan<Particle> Particles => _particles.AsSpan();
public Vector2 OriginPosition => _originPosition;
public ParticleEmitterSettings Settings => _settings;
public ParticleEmitter(Vector2 originPosition, ParticleEmitterSettings settings, ArraySegment<Particle> particles)
{
_originPosition = originPosition;
_settings = settings;
_maxParticles = _settings.MaxParticles;
_particleIndex = _maxParticles - 1;
_particles = particles;
_random = new LehmerRandom();
}
public void Update(double deltaTime)
{
var rate = (int)MathUtils.Lerp(1, _maxParticles, _settings.Explosiveness);
for (int i = 0; i < rate; i++)
{
Emit();
}
for (int i = 0; i < _maxParticles; i++)
{
var particle = _particles[i];
if (!particle.Alive) continue;
if (particle.LifeTimeRemaining <= 0.0f)
{
particle.Alive = false;
continue;
}
particle.LifeTimeRemaining = Math.Clamp(particle.LifeTimeRemaining - (float)deltaTime, 0.0f, particle.LifeTime);
var t = particle.LifeTimeRemaining / particle.LifeTime;
particle.Velocity += _settings.Gravity * (float)deltaTime;
particle.Position += particle.Velocity * (float)deltaTime;
particle.Rotation += particle.AngularVelocity * (float)deltaTime;
particle.Scale = MathUtils.Lerp(Settings.ScaleEnd, Settings.ScaleBegin, t);
var color = MathUtils.LerpColor(Settings.ColorEnd, Settings.ColorBegin, t);
particle.ColorArgb = color.Argb;
particle.Velocity -= particle.Velocity * _settings.Damping * (float)deltaTime;
_particles[i] = particle;
}
}
private void Emit()
{
Particle particle = _particles[_particleIndex];
if (!(particle.LifeTimeRemaining <= 0)) return;
particle.Alive = true;
particle.Position = GetEmitPosition();
particle.Velocity = _settings.Direction * _settings.LinearVelocity;
particle.Velocity += Vector2.One * _settings.LinearVelocityRandom * ((float)_random.NextDouble() - 0.5f);
particle.AngularVelocity = _settings.AngularVelocity;
particle.AngularVelocity += 1f * _settings.AngularVelocityRandom * ((float)_random.NextDouble() - 0.5f);
particle.LifeTime = _settings.LifeTime;
particle.LifeTimeRemaining = particle.LifeTime;
_particles[_particleIndex] = particle;
_particleIndex = --_particleIndex <= 0 ? _maxParticles - 1 : --_particleIndex;
}
private Vector2 GetEmitPosition()
{
// https://gamedev.stackexchange.com/questions/26713/calculate-random-points-pixel-within-a-circle-image
var angle = _random.NextDouble() * Math.PI * 2;
float radius = (float)Math.Sqrt(_random.NextDouble()) * _settings.EmitRadius;
float x = radius * (float)Math.Cos(angle);
float y = radius * (float)Math.Sin(angle);
return new Vector2(x, y);
}
private LehmerRandom _random;
private int _maxParticles;
private int _particleIndex;
private Vector2 _originPosition = Vector2.Zero;
private ArraySegment<Particle> _particles;
private ParticleEmitterSettings _settings;
}
public class ParticleSystem : IUpdatableSystem, IDisposable
{
/// <summary>
/// Maximum amount of particles emittable by the system.
/// </summary>
public int ParticleLimit { get; set; } = 8192;
public IReadOnlyList<ParticleEmitter> Emitters => _emitters;
public ParticleSystem()
{
_particleIndex = ParticleLimit - 1;
_particles = new Particle[ParticleLimit];
}
public void CreateEmitter(Vector2 originPosition, ParticleEmitterSettings settings)
{
if (_emitterSliceOffset + settings.MaxParticles >= ParticleLimit - 1)
{
_logger.Error("Cannot create an emitter! Reached particle limit.");
return;
}
var particles = new ArraySegment<Particle>(_particles, _emitterSliceOffset, settings.MaxParticles);
// foreach (var particle in particles)
// {
// particle.LifeTime = settings.LifeTime;
// }
for (int i = 0; i < particles.Count; i++)
{
var particle = particles[i];
particle.LifeTime = settings.LifeTime;
}
var emitter = new ParticleEmitter(originPosition, settings, particles);
_emitters.Add(emitter);
_emitterSliceOffset += settings.MaxParticles;
}
public void Update(double deltaTime)
{
foreach (var emitter in _emitters)
{
emitter.Update(deltaTime);
}
}
public void Dispose()
{
CleanupParticles();
}
private void CleanupParticles() => Array.Clear(_particles);
private Particle[] _particles;
private int _particleIndex;
private int _emitterSliceOffset;
private List<ParticleEmitter> _emitters = new();
private Logger _logger = new(nameof(ParticleSystem));
}