339 lines
12 KiB
C#
339 lines
12 KiB
C#
using System.Numerics;
|
|
using Voile.Resources;
|
|
using Voile.Resources.DataReaders;
|
|
using Voile.Utils;
|
|
|
|
namespace Voile.Systems.Particles;
|
|
|
|
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 Scale { get; set; }
|
|
public float Rotation { get; set; }
|
|
}
|
|
|
|
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 ParticleEmitterSettingsResource : Resource
|
|
{
|
|
public ParticleEmitterSettings Settings { get; private set; }
|
|
public ParticleEmitterSettingsResource(string path, ParticleEmitterSettings settings) : base(path)
|
|
{
|
|
Settings = settings;
|
|
}
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads <see cref="ParticleEmitterSettingsResource"/> from a provided TOML data file.
|
|
/// </summary>
|
|
public class ParticleEmitterSettingsResourceLoader : ResourceLoader<ParticleEmitterSettingsResource>
|
|
{
|
|
public override IEnumerable<string> SupportedExtensions => new string[] {
|
|
"toml"
|
|
};
|
|
|
|
protected override ParticleEmitterSettingsResource LoadResource(string path)
|
|
{
|
|
// TODO: this is ugly, better to make some sort of wrapper API for TOML files.
|
|
var settings = new ParticleEmitterSettings();
|
|
|
|
using (var reader = new TomlDataReader("ParticleEmitterSettings"))
|
|
{
|
|
reader.Read(File.Open(path, FileMode.Open));
|
|
|
|
settings.MaxParticles = reader.GetInt("MaxParticles");
|
|
settings.EmitRadius = reader.GetInt("EmitRadius");
|
|
settings.LifeTime = reader.GetFloat("LifeTime", 1.0f);
|
|
settings.Explosiveness = reader.GetFloat("Explosiveness");
|
|
settings.Direction = reader.GetVector2("Direction", Vector2.Zero);
|
|
settings.LinearVelocity = reader.GetFloat("LinearVelocity", 980f);
|
|
settings.AngularVelocity = reader.GetFloat("AngularVelocity");
|
|
settings.AngularVelocityRandom = reader.GetFloat("AngularVelocityRandom", 1.0f);
|
|
settings.LinearVelocityRandom = reader.GetFloat("LinearVelocityRandom", 1.0f);
|
|
settings.Gravity = reader.GetVector2("Gravity", Vector2.UnitY * 980f);
|
|
settings.ScaleBegin = reader.GetFloat("ScaleBegin");
|
|
settings.ScaleEnd = reader.GetFloat("ScaleEnd");
|
|
settings.ColorBegin = reader.GetColor("ColorBegin", Color.White);
|
|
settings.ColorEnd = reader.GetColor("ColorEnd", Color.Black);
|
|
settings.Damping = reader.GetFloat("Damping", 1.0f);
|
|
}
|
|
|
|
return new ParticleEmitterSettingsResource(path, settings);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits and simulates particles from a provided particle segment.
|
|
/// </summary>
|
|
public class ParticleEmitter : IUpdatableSystem
|
|
{
|
|
/// <summary>
|
|
/// Origin position in the world of this emitter.
|
|
/// </summary>
|
|
public Vector2 OriginPosition => _originPosition;
|
|
/// <summary>
|
|
/// <see cref="ParticleEmitterSettings"/> for this emitter.
|
|
/// </summary>
|
|
public ParticleEmitterSettings Settings => _settingsResource.Value.Settings;
|
|
public int ArrayOffset => _lifetimes.Offset;
|
|
|
|
/// <summary>
|
|
/// Constructs a new <see cref="ParticleEmitter"/>.
|
|
/// </summary>
|
|
/// <param name="originPosition">World origin position.</param>
|
|
/// <param name="settingsResource">Emitter settings resource.</param>
|
|
/// <param name="particles">Particle segment that this emitter will simulate.</param>
|
|
public ParticleEmitter(Vector2 originPosition, ResourceRef<ParticleEmitterSettingsResource> settingsResource, ArraySegment<Vector2> positionsSlice, ArraySegment<Vector2> velocitiesSlice, ArraySegment<float> lifetimesSlice)
|
|
{
|
|
_originPosition = originPosition;
|
|
|
|
_settingsResource = settingsResource;
|
|
_maxParticles = _settingsResource.Value.Settings.MaxParticles;
|
|
_particleIndex = _maxParticles - 1;
|
|
|
|
_positions = positionsSlice;
|
|
_velocities = velocitiesSlice;
|
|
_lifetimes = lifetimesSlice;
|
|
|
|
_random = new LehmerRandom();
|
|
}
|
|
|
|
public Particle GetParticle(int idx)
|
|
{
|
|
var t = _lifetimes[idx] / Settings.LifeTime;
|
|
return new Particle()
|
|
{
|
|
Position = _positions[idx],
|
|
Velocity = _velocities[idx],
|
|
Scale = MathUtils.Lerp(Settings.ScaleEnd, Settings.ScaleBegin, t),
|
|
ColorArgb = MathUtils.LerpColor(Settings.ColorEnd, Settings.ColorBegin, t).Argb
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Restart this emitter.
|
|
/// </summary>
|
|
/// <param name="particles">New particle segment.</param>
|
|
public void Restart()
|
|
{
|
|
for (int i = 0; i < _lifetimes.Count; i++)
|
|
{
|
|
_lifetimes[i] = 0.0f;
|
|
}
|
|
|
|
_maxParticles = Settings.MaxParticles;
|
|
_particleIndex = _maxParticles - 1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates this emitter's simulation.
|
|
/// </summary>
|
|
/// <param name="deltaTime"></param>
|
|
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];
|
|
|
|
_lifetimes[i] = Math.Max(0.0f, _lifetimes[i] - (float)deltaTime);
|
|
|
|
var t = _lifetimes[i] / Settings.LifeTime;
|
|
|
|
_velocities[i] += Settings.Gravity * (float)deltaTime;
|
|
_positions[i] += _velocities[i] * (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;
|
|
|
|
_velocities[i] -= _velocities[i] * Settings.Damping * (float)deltaTime;
|
|
}
|
|
}
|
|
|
|
private void Emit()
|
|
{
|
|
// Particle particle = _particles[_particleIndex];
|
|
|
|
if (_lifetimes[_particleIndex] > 0) return;
|
|
|
|
_positions[_particleIndex] = GetEmitPosition();
|
|
_velocities[_particleIndex] = Settings.Direction * Settings.LinearVelocity;
|
|
|
|
_velocities[_particleIndex] += Vector2.One * Settings.LinearVelocityRandom * ((float)_random.NextDouble() - 0.5f);
|
|
|
|
// particle.AngularVelocity = Settings.AngularVelocity;
|
|
// particle.AngularVelocity += 1f * Settings.AngularVelocityRandom * ((float)_random.NextDouble() - 0.5f);
|
|
|
|
_lifetimes[_particleIndex] = Settings.LifeTime;
|
|
|
|
_particleIndex = --_particleIndex <= 0 ? _maxParticles - 1 : --_particleIndex;
|
|
}
|
|
|
|
private Vector2 GetEmitPosition()
|
|
{
|
|
var settings = _settingsResource.Value.Settings;
|
|
|
|
// 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<Vector2> _positions, _velocities;
|
|
private ArraySegment<float> _lifetimes;
|
|
private ResourceRef<ParticleEmitterSettingsResource> _settingsResource;
|
|
}
|
|
|
|
/// <summary>
|
|
/// CPU based particle simulator.
|
|
/// </summary>
|
|
public class ParticleSystem : IUpdatableSystem, IDisposable
|
|
{
|
|
/// <summary>
|
|
/// Maximum amount of particles emittable by the system.
|
|
/// </summary>
|
|
public int ParticleLimit { get; set; } = 8192;
|
|
|
|
/// <summary>
|
|
/// List of particle emitters created for this ParticleSystem.
|
|
/// </summary>
|
|
public IReadOnlyList<ParticleEmitter> Emitters => _emitters;
|
|
|
|
/// <summary>
|
|
/// Constructs a new <see cref="ParticleSystem"/>.
|
|
/// </summary>
|
|
public ParticleSystem()
|
|
{
|
|
_particleIndex = ParticleLimit - 1;
|
|
|
|
_particlePositions = new Vector2[ParticleLimit];
|
|
_particleVelocities = new Vector2[ParticleLimit];
|
|
_particleLifetimes = new float[ParticleLimit];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an emitter from a <see cref="ParticleEmitterSettingsResource"/>.
|
|
/// </summary>
|
|
/// <param name="originPosition"></param>
|
|
/// <param name="settingsResource"></param>
|
|
/// <returns></returns>
|
|
public int CreateEmitter(Vector2 originPosition, ResourceRef<ParticleEmitterSettingsResource> settingsResource)
|
|
{
|
|
var settings = settingsResource.Value.Settings;
|
|
|
|
if (_emitterSliceOffset + settings.MaxParticles >= ParticleLimit - 1)
|
|
{
|
|
_logger.Error("Cannot create an emitter! Reached particle limit.");
|
|
return -1;
|
|
}
|
|
|
|
var positionsSlice = new ArraySegment<Vector2>(_particlePositions, _emitterSliceOffset, settings.MaxParticles);
|
|
var velocitiesSlice = new ArraySegment<Vector2>(_particleVelocities, _emitterSliceOffset, settings.MaxParticles);
|
|
var lifetimesSlice = new ArraySegment<float>(_particleLifetimes, _emitterSliceOffset, settings.MaxParticles);
|
|
|
|
for (int i = 0; i < lifetimesSlice.Count; i++)
|
|
{
|
|
lifetimesSlice[i] = settings.LifeTime;
|
|
}
|
|
|
|
var emitter = new ParticleEmitter(originPosition, settingsResource, positionsSlice, velocitiesSlice, lifetimesSlice);
|
|
|
|
_emitters.Add(emitter);
|
|
_emitterSliceOffset += settings.MaxParticles;
|
|
|
|
return _emitters.Count - 1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Restarts an emitter.
|
|
/// </summary>
|
|
/// <param name="id">Id of an emitter to restart.</param>
|
|
/// <exception cref="ArgumentException"></exception>
|
|
public void RestartEmitter(int id)
|
|
{
|
|
if (id > _emitters.Count - 1)
|
|
{
|
|
throw new ArgumentException($"Emitter with id {id} doesn't exist!");
|
|
}
|
|
|
|
var emitter = _emitters[id];
|
|
|
|
emitter.Restart();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates this particle simulation. It is recommended to use a fixed timestep for the particle simulation for performance reasons.
|
|
/// </summary>
|
|
/// <param name="deltaTime"></param>
|
|
public void Update(double deltaTime)
|
|
{
|
|
foreach (var emitter in _emitters)
|
|
{
|
|
emitter.Update(deltaTime);
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
CleanupParticles();
|
|
}
|
|
|
|
private void CleanupParticles()
|
|
{
|
|
Array.Clear(_particlePositions);
|
|
Array.Clear(_particleVelocities);
|
|
Array.Clear(_particleLifetimes);
|
|
}
|
|
// private Particle[] _particles;
|
|
private Vector2[] _particlePositions, _particleVelocities;
|
|
private float[] _particleLifetimes;
|
|
private int _particleIndex;
|
|
private int _emitterSliceOffset;
|
|
|
|
private List<ParticleEmitter> _emitters = new();
|
|
|
|
private Logger _logger = new(nameof(ParticleSystem));
|
|
} |