3using System.Runtime.CompilerServices;
77 private readonly
Particle[] _particles;
78 private readonly Random _rng =
new();
79 private readonly SKPaint _paint =
new() { IsAntialias =
true };
81 private long _particlesLastTick = 0;
84 private readonly SKBitmap? _particleSprite;
90 public readonly List<ParticleEmitter>
Emitters =
new();
191 string? nickname =
null,
192 int maxParticles = 2000,
193 SKBitmap? particleSprite =
null)
194 : base(renderSurfaceHost, mode, sceneLayer, view, screenBounds, worldBounds, nickname)
196 _particles = ArrayPool<Particle>.Shared.Rent(maxParticles);
197 _particleSprite = particleSprite;
213 string? nickname =
null,
214 int maxParticles = 2000,
215 SKBitmap? particleSprite =
null)
216 : this(renderSurfaceHost,
240 string? nickname =
null,
241 int maxParticles = 2000,
242 SKBitmap? particleSprite =
null)
243 : this(renderSurfaceHost,
279 protected override void Dispose(
bool disposing)
283 ArrayPool<Particle>.Shared.Return(_particles, clearArray:
true);
286 _particlesLastTick = 0;
288 base.Dispose(disposing);
304 if (_particlesLastTick > 0)
305 dt = HighResTimer.GetDuration(_particlesLastTick, tick);
307 _particlesLastTick = tick;
310 for (
int i = 0; i <
Emitters.Count; i++)
314 for (
int i = 0; i <
Emitters.Count; i++)
318 em._accumulator += rate * dt;
320 int toEmit = (int)em._accumulator;
321 if (toEmit > 0) { em._accumulator -= toEmit; EmitFrom(em, toEmit); }
326 for (
int i = 0; i < _alive; i++)
328 ref var p = ref _particles[i];
335 if (p.MaxVelocity > 0f)
338 float vx = p.VX, vy = p.VY;
339 float velocity2 = (vx * vx) + (vy * vy);
340 float max2 = p.MaxVelocity * p.MaxVelocity;
342 if (velocity2 > max2)
344 float inv = p.MaxVelocity / MathF.Sqrt(velocity2);
355 p.Rotation += p.AngularVel * dt;
368 if (p.Life > 0 && isInView)
369 _particles[write++] = p;
409 var canvas = backbuffer.
Canvas;
417 float sx = (srcBounds.Width > 0) ? (destRectScreen.Width / srcBounds.Width) : 1f;
418 float sy = (srcBounds.Height > 0) ? (destRectScreen.Height / srcBounds.Height) : 1f;
421 float sSize = (sx + sy) * 0.5f;
423 for (
int i = 0; i < _alive; i++)
425 ref var p = ref _particles[i];
428 _paint.BlendMode = p.BlendMode;
431 float t = 1f - (p.Life / p.MaxLife);
432 byte a = (byte)(255 * (1f - t));
438 _paint.Color = ApplyTint(p.Color, a, tint);
441 float x = destRectScreen.Left + (p.X - srcBounds.Left) * sx;
442 float y = destRectScreen.Top + (p.Y - srcBounds.Top) * sy;
444 if (p.ParticleSprite is
null)
446 float size = (p.Size * sSize) * (1f + 0.5f * t);
447 canvas.DrawCircle(x, y, size, _paint);
451 float s = p.Size * sSize;
452 var dst =
new SKRect(x - s * 0.5f, y - s * 0.5f,
453 x + s * 0.5f, y + s * 0.5f);
456 canvas.RotateDegrees(p.Rotation, x, y);
457 canvas.DrawBitmap(p.ParticleSprite, dst, _paint);
465 for (
int i = 0; i < count && _alive < _particles.Length; i++)
467 ref var p = ref _particles[_alive++];
477 (
float dx,
float dy) = SampleSpawnOffset(em);
478 p.X = em.Position.X + dx;
479 p.Y = em.Position.Y + dy;
487 p.AX = em.GravityX ?? this.GravityX;
488 p.AY = em.GravityY ?? this.GravityY;
496 p.Rotation = NextRange(0f, 360f);
497 p.AngularVel = NextRange(-180f, 180f);
500 p.ParticleSprite = em.ParticleSprite ?? this._particleSprite;
506 p.MaxVelocity = em.MaxVelocity ?? 0f;
516 private float NextRange(
float min,
float max) => (float)(_rng.NextDouble() * (max - min) + min);
518 private (
float dx,
float dy) SampleSpawnOffset(ParticleEmitter em)
520 float jx = MathF.Abs(em.JitterX);
521 float jy = MathF.Abs(em.JitterY);
525 return (0f, NextRange(-jy, jy));
528 return (NextRange(-jx, jx), 0f);
530 switch (em.SpawnDistribution)
534 return (NextRange(-jx, jx), NextRange(-jy, jy));
539 float a = NextRange(0f, MathF.Tau);
540 float r = MathF.Sqrt(NextRange(0f, 1f));
541 return (MathF.Cos(a) * r * jx, MathF.Sin(a) * r * jy);
547 float inner = Math.Clamp(em.RingInnerRadius01, 0f, 1f);
548 float a = NextRange(0f, MathF.Tau);
551 float u = NextRange(0f, 1f);
552 float r = MathF.Sqrt(inner * inner + u * (1f - inner * inner));
554 return (MathF.Cos(a) * r * jx, MathF.Sin(a) * r * jy);
560 float sd = Math.Clamp(em.GaussianStdDev01, 1e-4f, 10f);
562 float dx = NextGaussian() * (jx * sd);
563 float dy = NextGaussian() * (jy * sd);
568 float q = nx * nx + ny * ny;
572 float s = 1f / MathF.Sqrt(q);
582 private float NextGaussian()
586 double u1 = 1.0 - _rng.NextDouble();
587 double u2 = 1.0 - _rng.NextDouble();
588 double mag = Math.Sqrt(-2.0 * Math.Log(u1));
589 double z0 = mag * Math.Cos(Math.Tau * u2);
595 [MethodImpl(MethodImplOptions.AggressiveInlining)]
596 private SKColor ApplyTint(SKColor c,
byte lifeAlpha, SKColor tint)
598 int r = (c.Red * tint.Red) / 255;
599 int g = (c.Green * tint.Green) / 255;
600 int b = (c.Blue * tint.Blue) / 255;
601 int a = (c.Alpha * lifeAlpha * tint.Alpha) / (255 * 255);
603 return new SKColor((
byte)r, (
byte)g, (
byte)b, (
byte)a);
View? View
Gets the view to which this direct drawing is attached, or null if the drawing is in scene-layer mode...
long _lastTick
The last engine tick value used for delta-time calculations in Update.
SceneLayer? SceneLayer
Gets the scene layer to which this direct drawing is attached, or null if the drawing is in view mode...
Rectangle WorldBounds
Gets or sets the world-space bounds of this direct drawing in pixels.
DirectDrawingMode Mode
Gets the drawing mode that determines how this direct drawing is positioned and transformed.
Rectangle ScreenBounds
Gets or sets the screen-space bounds of this direct drawing in pixels.
DirectDrawingMovableBase(RenderSurfaceHostBase renderSurfaceHost, DirectDrawingMode mode, SceneLayer? sceneLayer, View? view, Rectangle? screenBounds, Rectangle? worldBounds, string? name=null)
Initializes a new instance of the DirectDrawingMovableBase class with integrated movement capabilitie...
Configures how particles are spawned over time at a given location.
SKColor? Tint
Optional per-emitter tint. If set, it overrides the particle system's GlobalColorTint for particles s...
float float Max VelocityRangeY
SKColor Color
Base color. Alpha will be modulated by lifetime fade. You can randomize in OnSpawn if desired.
float JitterX
Gets or sets the horizontal jitter value for spawning particles, which represents a random offset app...
SKBlendMode BlendMode
Blend mode used when rendering particles from this emitter. Defaults to SKBlendMode....
PointF Position
RenderHost / adapter pixel position where particles originate.
float float Max LifeRange
float float Max SizeRange
ParticleSpawnHandler? OnSpawn
Optional per-particle customization hook (e.g., perlin drift, hue jitter).
float float Max VelocityRangeX
float JitterY
Gets or sets the vertical jitter value for spawning particles, which represents a random offset appli...
A flexible particle system for effects like smoke, sparks, fire, or snow, implemented as a DirectDraw...
override void Update(long tick)
Tick-driven update override. Computes delta internally and advances simulation.
float GravityY
Gets or sets the gravitational acceleration along the Y-axis, measured in pixels per second squared....
ParticleSurface(RenderSurfaceHostBase renderSurfaceHost, View view, Rectangle? screenBounds, string? nickname=null, int maxParticles=2000, SKBitmap? particleSprite=null)
Initializes a new instance of the ParticleSurface class for view-based rendering with screen-space co...
override void Dispose(bool disposing)
Releases the particle array back to the pool and disposes managed resources.
readonly List< ParticleEmitter > Emitters
Collection of particle emitters this system updates and renders. Add multiple emitters (e....
ParticleSurface(RenderSurfaceHostBase renderSurfaceHost, SceneLayer sceneLayer, Rectangle? worldBounds, string? nickname=null, int maxParticles=2000, SKBitmap? particleSprite=null)
Initializes a new instance of the ParticleSurface class for scene layer rendering with world-space co...
void Burst(ParticleEmitter emitter, int count)
Immediately spawns a fixed number of particles from the given emitter. Useful for explosions,...
int ActiveParticleCount
Gets the current number of active (alive) particles being simulated.
SKColor GlobalColorTint
Global tint color multiplied against every particle's own color during rendering.
float GlobalEmitScale
Global multiplier applied to all emitter EmitRate values in this particle system. Acts like a master ...
float GravityX
Gets or sets the horizontal component of gravity, measured in pixels per second squared....
float CullingMarginY
Gets or sets the vertical culling margin, in pixels, beyond the bounds of the viewport to keep elemen...
float CullingMarginX
Gets or sets the horizontal culling margin, in pixels, beyond the bounds of the viewport to keep elem...
override void OnDraw(BackbufferBase backbuffer, RectangleF destRectScreen)
Renders all live particles to the current backbuffer.
Represents a base class for managing a graphical backbuffer, and is the in-memory surface where rende...
SKCanvas Canvas
Gets the SkiaSharp canvas used for drawing operations on this backbuffer.
Represents a base class for hosting a render surface, providing functionality for managing rendering ...
ParticleSpawnDistribution
Defines the spatial distribution pattern used when spawning particles within an emitter.
@ Rectangle
Particles are spawned uniformly within a rectangular area. This is the default distribution.
DirectDrawingMode
Specifies where direct drawing operations are applied within the rendering pipeline.
@ View
Performs direct drawing at the view level using screen-space coordinates.
@ SceneLayer
Performs direct drawing within a scene layer using world-space coordinates.
Represents a single particle instance in the particle system.