Code/ParticleModules.cs
using System;
using Sandbox;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace fxbox;
/// <summary>
/// Stage when a particle module executes
/// </summary>
public enum ModuleStage
{
Spawn, // Controls when/how particles spawn
Initialize, // Runs once when particle is created
Update, // Runs every frame for each particle
Render // Controls how particles are rendered
}
/// <summary>
/// Context passed to particle modules during execution
/// </summary>
public class ParticleExecutionContext
{
public Particle Particle;
public ParticleEffect Effect;
public Sandbox.ParticleEmitter Emitter;
public FXBoxNativeParticleSystem SystemComponent; // Changed from Resource to SystemComponent
}
// ==================== SPAWN MODULES ====================
/// <summary>
/// Controls spawn rate over time
/// </summary>
[Title("Spawn Rate"), Category("Spawn"), Icon("speed")]
public partial class SpawnRateModule : ParticleModule
{
[Hide]
public override ModuleStage Stage => ModuleStage.Spawn;
[Property, Range(0.1f, 1000f)]
public FXParticleFloat SpawnRate { get; set; } = 10.0f;
public override void Execute(ParticleExecutionContext context)
{
var rate = SpawnRate;
context.Emitter.Rate = rate.GetValue( context.SystemComponent );
}
public override void Initialize( ParticleExecutionContext context )
{
context.Emitter.Rate = SpawnRate.GetValue( context.SystemComponent );
}
}
/// <summary>
/// Sets initial particle stretch
/// </summary>
[Title("Particle Stretch"), Category("Initialize"), Icon("photo_size_select_small")]
public partial class ParticleStretchModule : ParticleModule
{
[Hide]
public override ModuleStage Stage => ModuleStage.Initialize;
[Property, Range(0.1f, 100f)]
public FXParticleFloat Size { get; set; } = 1.0f;
public override void Initialize( ParticleExecutionContext context )
{
context.Effect.ApplyShape = true;
context.Effect.Stretch = Size.ToParticleFloat( context.SystemComponent );
}
public override void Execute(ParticleExecutionContext context)
{
}
}
/// <summary>
/// Controls spawn rate per unit
/// </summary>
[Title("Spawn Rate Over Distance"), Category("Spawn"), Icon("speed")]
public partial class SpawnRateOverDistanceModule : ParticleModule
{
[Hide]
public override ModuleStage Stage => ModuleStage.Spawn;
[Property, Range(0.1f, 1000f)]
public FXParticleFloat SpawnRate { get; set; } = 10.0f;
public override void Execute(ParticleExecutionContext context)
{
context.Emitter.RateOverDistance = SpawnRate.GetValue( context.SystemComponent );
}
public override void Initialize( ParticleExecutionContext context )
{
context.Emitter.RateOverDistance = SpawnRate.GetValue( context.SystemComponent );
}
}
/// <summary>
/// Spawns particles in a burst
/// </summary>
[Title("Spawn Burst"), Category("Spawn"), Icon("auto_awesome")]
public partial class SpawnBurstModule : ParticleModule
{
[Hide]
public override ModuleStage Stage => ModuleStage.Spawn;
[Property, Range(1, 1000)]
public int ParticleCount { get; set; } = 50;
public override void Initialize( ParticleExecutionContext context )
{
context.Emitter.Burst = ParticleCount;
}
public override void Execute(ParticleExecutionContext context)
{
context.Emitter.Burst = ParticleCount;
}
}
// ==================== INITIALIZE MODULES ====================
/// <summary>
/// Sets initial position based on shape
/// </summary>
[Title("Initialize Position"), Category("Initialize"), Icon("place")]
public partial class InitializePositionModule : ParticleModule, IParticleComponentCreator
{
[Hide]
public override ModuleStage Stage => ModuleStage.Initialize;
public override void Execute( ParticleExecutionContext context )
{
}
public enum SpawnShape { Point, Sphere, Box, Cone, Circle, Line }
[Property]
public FXCopyFlags CopyFlags { get; set; } = FXCopyFlags.Rotation | FXCopyFlags.Scale;
[Property]
public SpawnShape Shape { get; set; } = SpawnShape.Sphere;
[Property, Range(0f, 1000f), ShowIf(nameof(ShowRadius), true)]
public float Radius { get; set; } = 50.0f;
[Property, ShowIf(nameof(ShowBoxSize), true)]
public Vector3 BoxSize { get; set; } = new Vector3(100, 100, 100);
[Property, Range(0f, 180f), ShowIf(nameof(ShowConeAngle), true)]
public float ConeAngle { get; set; } = 45.0f;
[Property]
public bool EmitFromShell { get; set; } = false;
[Property, ShowIf(nameof(ShowLine), true)]
public Vector3 LineStart { get; set; } = Vector3.Zero;
[Property, ShowIf(nameof(ShowLine), true)]
public Vector3 LineEnd { get; set; } = Vector3.Up * 100;
[Hide] public bool ShowRadius => Shape == SpawnShape.Sphere || Shape == SpawnShape.Circle || Shape == SpawnShape.Cone;
[Hide] public bool ShowBoxSize => Shape == SpawnShape.Box;
[Hide] public bool ShowConeAngle => Shape == SpawnShape.Cone;
[Hide] public bool ShowLine => Shape == SpawnShape.Line;
public override void Initialize( ParticleExecutionContext context )
{
if ( context.Particle != null )
{
var pos = Shape switch
{
SpawnShape.Point => Vector3.Zero,
SpawnShape.Sphere => GetSpherePosition(),
SpawnShape.Box => GetBoxPosition(),
SpawnShape.Cone => GetConePosition(),
SpawnShape.Circle => GetCirclePosition(),
SpawnShape.Line => GetLinePosition(),
_ => Vector3.Zero
};
if ( CopyFlags.HasFlag( FXCopyFlags.Scale ) )
{
pos = pos * context.Emitter.WorldScale;
}
if ( CopyFlags.HasFlag( FXCopyFlags.Rotation ) )
{
pos = pos.RotateAround( 0, context.SystemComponent.WorldRotation );
}
context.Particle.Position += pos;
}
}
public void CreateComponent(GameObject go)
{
var pointEmitter = go.AddComponent<ParticleSphereEmitter>();
pointEmitter.Radius = 0;
pointEmitter.Velocity = 0;
pointEmitter.Burst = 0;
pointEmitter.Rate = 0;
}
private Vector3 GetSpherePosition()
{
var direction = Random.Shared.VectorInSphere().Normal;
var radius = EmitFromShell ? Radius : Random.Shared.Float(0, Radius);
return direction * radius;
}
private Vector3 GetBoxPosition()
{
if (EmitFromShell)
{
var face = Random.Shared.Int(0, 5);
var u = Random.Shared.Float(0, 1);
var v = Random.Shared.Float(0, 1);
var halfSize = BoxSize / 2f;
return face switch
{
0 => new Vector3(-halfSize.x, MathX.Lerp(-halfSize.y, halfSize.y, u), MathX.Lerp(-halfSize.z, halfSize.z, v)),
1 => new Vector3(halfSize.x, MathX.Lerp(-halfSize.y, halfSize.y, u), MathX.Lerp(-halfSize.z, halfSize.z, v)),
2 => new Vector3(MathX.Lerp(-halfSize.x, halfSize.x, u), -halfSize.y, MathX.Lerp(-halfSize.z, halfSize.z, v)),
3 => new Vector3(MathX.Lerp(-halfSize.x, halfSize.x, u), halfSize.y, MathX.Lerp(-halfSize.z, halfSize.z, v)),
4 => new Vector3(MathX.Lerp(-halfSize.x, halfSize.x, u), MathX.Lerp(-halfSize.y, halfSize.y, v), -halfSize.z),
_ => new Vector3(MathX.Lerp(-halfSize.x, halfSize.x, u), MathX.Lerp(-halfSize.y, halfSize.y, v), halfSize.z)
};
}
return new Vector3(
Random.Shared.Float(-BoxSize.x / 2, BoxSize.x / 2),
Random.Shared.Float(-BoxSize.y / 2, BoxSize.y / 2),
Random.Shared.Float(-BoxSize.z / 2, BoxSize.z / 2)
);
}
private Vector3 GetConePosition()
{
var angle = Random.Shared.Float(0, 360);
var distance = Random.Shared.Float(0, Radius);
var coneRadius = MathF.Tan(ConeAngle.DegreeToRadian()) * distance;
var radius = EmitFromShell ? coneRadius : Random.Shared.Float(0, coneRadius);
return new Vector3(
MathF.Cos(angle.DegreeToRadian()) * radius,
MathF.Sin(angle.DegreeToRadian()) * radius,
distance
);
}
private Vector3 GetCirclePosition()
{
var angle = Random.Shared.Float(0, 360);
var radius = EmitFromShell ? Radius : Random.Shared.Float(0, Radius);
return new Vector3(
MathF.Cos(angle.DegreeToRadian()) * radius,
MathF.Sin(angle.DegreeToRadian()) * radius,
0
);
}
private Vector3 GetLinePosition()
{
return Vector3.Lerp(LineStart, LineEnd, Random.Shared.Float(0, 1));
}
}
/// <summary>
/// Sets initial velocity
/// </summary>
[Title("Initialize Velocity"), Category("Initialize"), Icon("air")]
public partial class InitializeVelocityModule : ParticleModule
{
[Hide]
public override ModuleStage Stage => ModuleStage.Initialize;
[Property]
public FXParticleVector Velocity { get; set; } = Vector3.Up * 100;
[Property] public bool LocalSpace { get; set; } = false;
[Property]
public bool InheritEmitterVelocity { get; set; } = false;
[Property,ShowIf("InheritEmitterVelocity",true)] public float EmitterVelocityScale { get; set; } = 1.0f;
public override void Initialize( ParticleExecutionContext context )
{
var startVelocity = Velocity;
if ( LocalSpace )
{
startVelocity = startVelocity.GetValue( context.Particle,context.SystemComponent ).RotateAround( 0,context.Emitter.WorldRotation );
}
context.Effect.InitialVelocity = startVelocity.GetValue( context.Particle,context.SystemComponent );
if ( InheritEmitterVelocity )
{
context.Effect.InitialVelocity = (context.SystemComponent.Velocity*EmitterVelocityScale) + startVelocity.GetValue( context.Particle,context.SystemComponent );
}
}
public override void Execute(ParticleExecutionContext context)
{
}
}
/// <summary>
/// Sets initial color
/// </summary>
[Title("Collision"), Category("Initialize"), Icon("palette")]
public partial class ParticleCollisionModule : ParticleModule
{
[Property] public TagSet CollisionIgnore { get; set; } = new TagSet();
[Property] public List<GameObject> CollisionPrefabs { get; set; } = new List<GameObject>();
[Property] public FXParticleFloat CollisionRadius { get; set; } = 5;
[Property] public FXParticleFloat CollisionPrefabChance { get; set; } = 1;
[Property] public FXParticleFloat CollisionPrefabRotation { get; set; } = 0;
[Property] public FXParticleFloat DieOnCollisionChance { get; set; } = 0;
[Property] public bool CollisionPrefabAlign { get; set; } = false;
[Hide]
public override ModuleStage Stage => ModuleStage.Initialize;
public override void Execute(ParticleExecutionContext context)
{
}
public override void Initialize( ParticleExecutionContext context )
{
context.Effect.Collision = true;
context.Effect.CollisionIgnore = CollisionIgnore;
context.Effect.CollisionRadius = CollisionRadius.GetValue( context.SystemComponent );
context.Effect.CollisionPrefabChance = CollisionPrefabChance.GetValue( context.SystemComponent );
context.Effect.CollisionPrefabRotation = CollisionPrefabRotation.ToParticleFloat( context.SystemComponent );
if ( CollisionPrefabs.Any() )
{
context.Effect.UsePrefabFeature = true;
}
context.Effect.CollisionPrefab = CollisionPrefabs;
context.Effect.DieOnCollisionChance = DieOnCollisionChance.GetValue( context.SystemComponent );
context.Effect.CollisionPrefabAlign = CollisionPrefabAlign;
}
}
/// <summary>
/// Sets initial velocity
/// </summary>
[Title("Initialize Rotation"), Category("Initialize"), Icon("air")]
public partial class InitializeRotationModule : ParticleModule
{
[Hide]
public override ModuleStage Stage => ModuleStage.Initialize;
[Property]
public ParticleVector3 InitialRotation { get; set; } = Vector3.Up;
public override void Initialize( ParticleExecutionContext context )
{
if ( context.Particle != null )
{
context.Particle.Angles = new Angles( InitialRotation.Evaluate( Time.Delta,context.Particle.Rand( ),context.Particle.Rand( ),context.Particle.Rand( ) ) );
}
}
public override void Execute(ParticleExecutionContext context)
{
}
}
/// <summary>
/// Sets initial lifetime
/// </summary>
[Title("Initialize Lifetime"), Category("Initialize"), Icon("schedule")]
public partial class InitializeLifetimeModule : ParticleModule
{
[Hide]
public override ModuleStage Stage => ModuleStage.Initialize;
[Property, Range(0.1f, 100f)]
public FXParticleFloat Lifetime { get; set; } = 2.0f;
public override void Initialize(ParticleExecutionContext context)
{
context.Effect.Lifetime = Lifetime.ToParticleFloat( context.SystemComponent );
}
public override void Execute(ParticleExecutionContext context)
{
// Not used
}
}
/// <summary>
/// Sets initial size
/// </summary>
[Title("Initialize Size"), Category("Initialize"), Icon("photo_size_select_small")]
public partial class InitializeSizeModule : ParticleModule
{
[Hide]
public override ModuleStage Stage => ModuleStage.Initialize;
[Property] public bool InheritEmitterScale { get; set; } = true;
[Property, Range(0.1f, 100f)]
public FXParticleFloat Size { get; set; } = 10.0f;
public override void Initialize( ParticleExecutionContext context )
{
context.Effect.ApplyShape = true;
if ( InheritEmitterScale )
{
context.Effect.Scale = Size.ToParticleFloat( context.SystemComponent );
}
else
{
context.Effect.Scale = (Size / context.SystemComponent.WorldScale.x).ToParticleFloat( context.SystemComponent );
}
}
public override void Execute(ParticleExecutionContext context)
{
}
}
/// <summary>
/// Sets initial color
/// </summary>
[Title("Initialize Color"), Category("Initialize"), Icon("palette")]
public partial class InitializeColorModule : ParticleModule
{
[Hide]
public override ModuleStage Stage => ModuleStage.Initialize;
[Property] public FXParticleColor Color { get; set; } = global::Color.Red;
public override void Execute(ParticleExecutionContext context)
{
}
public override void Initialize( ParticleExecutionContext context )
{
context.Effect.ApplyColor = true;
context.Effect.ApplyAlpha = true;
if ( Color != null )
{
context.Effect.Gradient = Color.GetValue( context.SystemComponent );
}
else
{
Color = new FXParticleColor( global::Color.Red );
}
}
}
/// <summary>
/// Sets initial color
/// </summary>
[Title("Sprite Flipbook"), Category("Initialize"), Icon("palette")]
public partial class SpriteFlipbookModule : ParticleModule
{
[Property] public FXParticleFloat SequenceTime { get; set; } = 0;
[Property] public FXParticleFloat SequenceSpeed { get; set; } = 1;
[Property] public int SequenceId { get; set; } = 0;
[Hide]
public override ModuleStage Stage => ModuleStage.Initialize;
public override void Execute(ParticleExecutionContext context)
{
}
public override void Initialize( ParticleExecutionContext context )
{
context.Effect.SheetSequence = true;
context.Effect.SequenceId = SequenceId;
context.Effect.SequenceSpeed = SequenceSpeed.ToParticleFloat( context.SystemComponent );
context.Effect.SequenceTime = SequenceTime.ToParticleFloat( context.SystemComponent );
}
}
/// <summary>
/// Randomly Kill a particle to spawn less
/// </summary>
[Title("RandomKill"), Category("Initialize"), Icon("arrow_downward")]
public partial class RandomKill : ParticleModule
{
[Hide]
public override ModuleStage Stage => ModuleStage.Initialize;
[Property]
public float Chance { get; set; } = 0.5f;
public override void Execute(ParticleExecutionContext context)
{
}
public override void Initialize( ParticleExecutionContext context )
{
if ( context.Particle == null ) return;
if ( Random.Shared.Float( 0, 1 ) < Chance )
{
context.Particle.Age = 100000;
}
}
}
// ==================== UPDATE MODULES ====================
/// <summary>
/// Applies gravity force
/// </summary>
[Title("Gravity Force"), Category("Update"), Icon("arrow_downward")]
public partial class GravityForceModule : ParticleModule, IParticleUpdater
{
[Hide]
public override ModuleStage Stage => ModuleStage.Update;
[Property]
public FXParticleVector Force { get; set; } = new Vector3(0, 0, -980);
public override void Execute(ParticleExecutionContext context)
{
}
public override void Initialize( ParticleExecutionContext context )
{
}
public void UpdateParticle(ParticleExecutionContext context, float delta)
{
context.Particle.Velocity += Force.GetValue(context.Particle, context.SystemComponent ) * Time.Delta;
}
}
/// <summary>
/// Make a mesh follow it's velocity
/// </summary>
[Title("Follow Velocity"), Category("Update"), Icon("arrow_downward")]
public partial class FollowVelocity : ParticleModule, IParticleUpdater
{
[Hide]
public override ModuleStage Stage => ModuleStage.Update;
public override void Execute(ParticleExecutionContext context)
{
}
public override void Initialize( ParticleExecutionContext context )
{
}
public void UpdateParticle(ParticleExecutionContext context, float delta)
{
context.Particle.Angles = Rotation.LookAt( context.Particle.Velocity ).Angles();
}
}
/// <summary>
/// Applies drag/air resistance
/// </summary>
[Title("Drag Force"), Category("Update"), Icon("air")]
public partial class DragForceModule : ParticleModule, IParticleUpdater
{
[Hide]
public override ModuleStage Stage => ModuleStage.Update;
[Property, Range(0f, 10f)]
public float Damping { get; set; } = 0.1f;
public override void Execute(ParticleExecutionContext context)
{
}
public override void Initialize( ParticleExecutionContext context )
{
}
public void UpdateParticle(ParticleExecutionContext context, float delta)
{
context.Particle.Velocity *= (1.0f - Damping * Time.Delta);
}
}
/// <summary>
/// Makes particles rotate
/// </summary>
[Title("Rotation"), Category("Update"), Icon("rotate_right")]
public partial class RotationModule : ParticleModule, IParticleUpdater
{
[Hide]
public override ModuleStage Stage => ModuleStage.Update;
[Property, Range(-360f, 360f)]
public FXParticleFloat RotationSpeed { get; set; } = 90.0f;
public override void Execute(ParticleExecutionContext context)
{
/*context.Particle.Rotation += RotationSpeed * context.DeltaTime;*/
}
public override void Initialize( ParticleExecutionContext context )
{
}
public void UpdateParticle(ParticleExecutionContext context, float delta)
{
context.Particle.Angles += RotationSpeed.GetValue( context.SystemComponent ) * Time.Delta;
}
}
public enum PositionType
{
Local,
World
}
/// <summary>
/// Attracts particles to a point. Full strength inside AttractorSize, falling off beyond it.
/// </summary>
[Title("Point Attractor"), Category("Update"), Icon("my_location")]
public partial class PointAttractorModule : ParticleModule, IParticleUpdater
{
[Hide]
public override ModuleStage Stage => ModuleStage.Update;
[Property] public PositionType PositionType { get; set; } = PositionType.Local;
[Property]
public FXParticleVector AttractorPosition { get; set; } = Vector3.Zero;
[Property, Range(0f, 10000f)]
public FXParticleFloat Strength { get; set; } = 500.0f;
[Property, Range(0.01f, 10000f)]
public float AttractorSize { get; set; } = 50.0f;
[Property] public bool Invert { get; set; } = false;
/// <summary>
/// How quickly strength falls off beyond AttractorSize.
/// 1 = linear, 2 = inverse square, higher = sharper falloff.
/// </summary>
[Property, Range(0.1f, 8f)]
public float Falloff { get; set; } = 2.0f;
public override void Execute(ParticleExecutionContext context) { }
public override void Initialize(ParticleExecutionContext context) { }
public void UpdateParticle(ParticleExecutionContext context, float delta)
{
var attractorPos = PositionType == PositionType.Local ? AttractorPosition.GetValue( context.Particle, context.SystemComponent ) + context.Emitter.WorldPosition : AttractorPosition.GetValue( context.Particle, context.SystemComponent );
var toAttractor = attractorPos - context.Particle.Position;
var distance = toAttractor.Length;
if (distance < 0.01f) return;
// Inside the attractor: full strength.
// Outside: strength falls off based on normalised excess distance.
float strengthMultiplier;
if (distance <= AttractorSize)
{
strengthMultiplier = 1f;
}
else
{
// How many radii past the edge are we? 0 at the surface, grows outward.
var excess = (distance - AttractorSize) / AttractorSize;
strengthMultiplier = 1f / MathF.Pow(1f + excess, Falloff);
}
if ( Invert )
{
strengthMultiplier = 1 - strengthMultiplier;
}
context.Particle.Velocity += toAttractor.Normal * Strength.GetValue( context.SystemComponent ) * strengthMultiplier * Time.Delta;
}
}
/// <summary>
/// Creates orbital motion
/// </summary>
[Title("Vortex Force"), Category("Update"), Icon("cyclone")]
public partial class VortexForceModule : ParticleModule, IParticleUpdater
{
[Hide]
public override ModuleStage Stage => ModuleStage.Update;
[Property]
public Vector3 Center { get; set; } = Vector3.Zero;
[Property]
public FXParticleVector Axis { get; set; } = Vector3.Up;
[Property, Range(0f, 1000f)]
public float Strength { get; set; } = 100.0f;
public override void Execute(ParticleExecutionContext context)
{
}
public override void Initialize( ParticleExecutionContext context )
{
}
public void UpdateParticle(ParticleExecutionContext context, float delta)
{
var toCenter = context.Particle.Position - (Center + context.Emitter.WorldPosition);
var distance = toCenter.Length;
if (distance > 0.01f)
{
var tangent = Vector3.Cross(Axis.GetValue( context.Particle,context.SystemComponent ).Normal, toCenter.Normal);
var force = tangent * (Strength / distance);
context.Particle.Velocity += force * Time.Delta * 10000;
}
}
}
// ==================== RENDER MODULES ====================
/// <summary>
/// Basic sprite renderer
/// </summary>
[Title("Sprite Renderer"), Category("Render"), Icon("image")]
public partial class SpriteRendererModule : ParticleModule, IParticleComponentCreator
{
[Hide]
public override ModuleStage Stage => ModuleStage.Render;
[Property]
public Sprite Sprite { get; set; }
[Property]
public ParticleSpriteRenderer.BillboardAlignment Alignment { get; set; } =
ParticleSpriteRenderer.BillboardAlignment.LookAtCamera;
[Property] public bool FaceVelocity { get; set; } = false;
[Property] public bool Additive { get; set; } = false;
public override void Execute(ParticleExecutionContext context)
{
}
public override void Initialize( ParticleExecutionContext context )
{
}
public void CreateComponent(GameObject go)
{
var renderer = go.AddComponent<ParticleSpriteRenderer>();
renderer.Alignment = Alignment;
renderer.FaceVelocity = FaceVelocity;
renderer.Sprite = Sprite;
renderer.Additive = Additive;
}
}
/// <summary>
/// Basic light renderer
/// </summary>
[Title("Light Renderer"), Category("Render"), Icon("image")]
public partial class LightRendererModule : ParticleModule, IParticleComponentCreator
{
[Hide]
public override ModuleStage Stage => ModuleStage.Render;
[Property] public FXParticleColor LightColor { get; set; } = new FXParticleColor( Color.White );
[Property] public FXParticleFloat Brightness { get; set; } = 10f;
[Property] public FXParticleFloat MaxLights { get; set; } = 10f;
[Property] public FXParticleFloat LightSize { get; set; } = 10f;
[Property] public FXParticleFloat Attenuation { get; set; } = 1;
[Property] public bool CastShadows { get; set; } = false;
[Property] public FXParticleFloat Ratio { get; set; } = 1;
public override void Execute(ParticleExecutionContext context)
{
// Rendering is handled externally, this just stores render properties
}
public override void Initialize( ParticleExecutionContext context )
{
}
public void CreateComponent(GameObject go)
{
var renderer = go.AddComponent<ParticleLightRenderer>();
var fxbox=go.GetComponentInParent<FXBoxNativeParticleSystem>( );
renderer.LightColor = LightColor.GetValue( fxbox );
renderer.Brightness = Brightness.GetValue( fxbox );
renderer.MaximumLights = (int)MaxLights.GetValue( fxbox );
renderer.Scale = LightSize.GetValue( fxbox );
renderer.Attenuation = Attenuation.GetValue( fxbox );
renderer.Ratio = Ratio.GetValue( go.GetComponentInParent<FXBoxNativeParticleSystem>() );
renderer.CastShadows = CastShadows;
}
}
/// <summary>
/// Basic model renderer
/// </summary>
[Title("Model Renderer"), Category("Render"), Icon("image")]
public partial class ModelRendererModule : ParticleModule, IParticleComponentCreator
{
[Hide]
public override ModuleStage Stage => ModuleStage.Render;
[Property]
public List<ParticleModelRenderer.ModelEntry> Models { get; set; }
[Property]
public bool FaceCamera { get; set; } = true;
public override void Execute(ParticleExecutionContext context)
{
// Rendering is handled externally, this just stores render properties
}
public override void Initialize( ParticleExecutionContext context )
{
}
public void CreateComponent(GameObject go)
{
var renderer = go.AddComponent<ParticleModelRenderer>();
renderer.Choices = Models;
}
}
/// <summary>
/// Basic Trail Renderer
/// </summary>
[Title( "Trail Renderer" ), Category( "Render" ), Icon( "image" )]
public partial class TrailRendererModule : ParticleModule, IParticleComponentCreator
{
[Hide] public override ModuleStage Stage => ModuleStage.Render;
[Property] public bool Game { get; set; } = true;
[Property] public bool Overlay { get; set; } = false;
[Property] public bool Bloom { get; set; } = false;
[Property] public bool AfterUi { get; set; } = false;
[Property] public Material Material { get; set; }
[Property] public FXParticleFloat UnitsPerTexture { get; set; } = 10f;
[Property] public FXParticleFloat Scroll { get; set; } = 0f;
[Property] public FXParticleFloat Width { get; set; } = 1f;
[Property] public bool Opaque { get; set; } = true;
[Property, ShowIf( "Opaque", false )] public BlendMode BlendMode { get; set; } = BlendMode.Normal;
[Property] public int MaxPoints { get; set; } = 32;
[Property] public float PointDistance { get; set; } = 8;
[Property] public float LifeTime { get; set; } = 2f;
[Property] public FXParticleColor Color { get; set; } = new FXParticleColor( );
public override void Execute(ParticleExecutionContext context)
{
// Rendering is handled externally, this just stores render properties
}
public override void Initialize( ParticleExecutionContext context )
{
}
public void CreateComponent(GameObject go)
{
var renderer = go.AddComponent<ParticleTrailRenderer>();
var appearance = renderer.Texturing;
appearance.Material = Material;
appearance.UnitsPerTexture = UnitsPerTexture.GetValue( );
appearance.Scroll = Scroll.GetValue();
var widthCurve = Width.ToParticleFloat();
if ( widthCurve.Type == ParticleFloat.ValueType.Curve )
{
renderer.Width = widthCurve.CurveA;
} else if ( widthCurve.Type == ParticleFloat.ValueType.Range )
{
var point1 = new Curve.Frame( 0, widthCurve.ConstantA );
var point2 = new Curve.Frame( 1, widthCurve.ConstantB );
renderer.Width = new Curve( point1, point2 );
} else if ( widthCurve.Type == ParticleFloat.ValueType.Constant )
{
renderer.Width = widthCurve.ConstantA;
}
else
{
renderer.Width = widthCurve.CurveA;
}
renderer.Opaque = Opaque;
renderer.BlendMode = BlendMode;
renderer.MaxPoints = MaxPoints;
renderer.PointDistance = PointDistance;
renderer.LifeTime = LifeTime;
var colorParam = Color.GetValue();
if ( colorParam.Type == ParticleGradient.ValueType.Constant )
{
renderer.Color = colorParam.ConstantA;
} else if ( colorParam.Type == ParticleGradient.ValueType.Range )
{
var point1 = new Gradient.ColorFrame( 0, colorParam.ConstantA );
var point2 = new Gradient.ColorFrame( 1, colorParam.ConstantB );
renderer.Color = new Gradient( point1, point2 );
} else if ( colorParam.Type == ParticleGradient.ValueType.Gradient )
{
renderer.Color = colorParam.GradientA;
}
renderer.RenderOptions.Game = Game;
renderer.RenderOptions.Overlay = Overlay;
renderer.RenderOptions.Bloom = Bloom;
renderer.RenderOptions.AfterUI = AfterUi;
renderer.Texturing = appearance;
}
}
/// <summary>
/// Applies curl noise force for organic, swirling motion
/// </summary>
[Title("Curl Noise"), Category("Update"), Icon("air")]
public partial class CurlNoiseModule : ParticleModule, IParticleUpdater
{
[Hide]
public override ModuleStage Stage => ModuleStage.Update;
[Property, Range(0f, 1000f)]
[Description("Strength of the curl noise effect")]
public FXParticleFloat Strength { get; set; } = 1.0f;
[Property, Range(0.01f, 10f)]
[Description("Scale of the noise pattern - smaller values create tighter curls")]
public FXParticleFloat Scale { get; set; } = 1.0f;
[Property, Range(0f, 10f)]
[Description("Speed at which the noise pattern evolves over time")]
public FXParticleFloat TimeScale { get; set; } = 1.0f;
[Property]
[Description("Offset in the noise field")]
public Vector3 Offset { get; set; } = Vector3.Zero;
public override void Execute(ParticleExecutionContext context)
{
// Not used - handled in UpdateParticle
}
public override void Initialize(ParticleExecutionContext context)
{
// No initialization needed
}
public void UpdateParticle(ParticleExecutionContext context, float delta)
{
var particle = context.Particle;
// Sample position in noise field
var samplePos = (particle.Position + Offset) * Scale.GetValue( context.SystemComponent );
var time = context.Particle.Age * TimeScale;
// Calculate curl noise using the curl of a 3D noise field
var curl = CalculateCurl(samplePos, time.GetValue( context.SystemComponent ));
// Apply force
particle.Velocity += curl * Strength.GetValue( context.SystemComponent ) * delta * 10;
}
/// <summary>
/// Calculate curl noise by taking the curl of a potential field
/// This creates divergence-free flow fields that look organic
/// </summary>
private Vector3 CalculateCurl(Vector3 pos, float time)
{
const float epsilon = 0.001f;
// Sample the potential field at offset positions
// We need 6 samples to calculate the curl (derivatives in all directions)
// dPz/dy - dPy/dz
float curlX =
(SamplePotential(pos + new Vector3(0, epsilon, 0), time).z -
SamplePotential(pos - new Vector3(0, epsilon, 0), time).z) -
(SamplePotential(pos + new Vector3(0, 0, epsilon), time).y -
SamplePotential(pos - new Vector3(0, 0, epsilon), time).y);
// dPx/dz - dPz/dx
float curlY =
(SamplePotential(pos + new Vector3(0, 0, epsilon), time).x -
SamplePotential(pos - new Vector3(0, 0, epsilon), time).x) -
(SamplePotential(pos + new Vector3(epsilon, 0, 0), time).z -
SamplePotential(pos - new Vector3(epsilon, 0, 0), time).z);
// dPy/dx - dPx/dy
float curlZ =
(SamplePotential(pos + new Vector3(epsilon, 0, 0), time).y -
SamplePotential(pos - new Vector3(epsilon, 0, 0), time).y) -
(SamplePotential(pos + new Vector3(0, epsilon, 0), time).x -
SamplePotential(pos - new Vector3(0, epsilon, 0), time).x);
return new Vector3(curlX, curlY, curlZ) / (2.0f * epsilon);
}
/// <summary>
/// Sample a 3D potential field using Perlin-like noise
/// </summary>
private Vector3 SamplePotential(Vector3 pos, float time)
{
// Create three offset noise samples for each component
// This creates a vector field from scalar noise functions
return new Vector3(
Noise3D(pos + new Vector3(0, 0, 0), time),
Noise3D(pos + new Vector3(31.416f, -47.853f, 12.793f), time),
Noise3D(pos + new Vector3(-17.737f, 86.214f, -59.482f), time)
);
}
/// <summary>
/// Simple 3D noise function using sine waves
/// You could replace this with proper Perlin/Simplex noise for better results
/// </summary>
private float Noise3D(Vector3 pos, float time)
{
// Combine multiple sine waves at different frequencies for pseudo-noise
var p = pos + new Vector3(time, time * 0.7f, time * 0.5f);
float noise = 0;
noise += MathF.Sin(p.x * 1.0f + p.y * 1.3f) * 0.5f;
noise += MathF.Sin(p.y * 1.7f + p.z * 0.9f) * 0.3f;
noise += MathF.Sin(p.z * 2.1f + p.x * 1.1f) * 0.2f;
noise += MathF.Sin(p.x * 3.7f + p.y * 2.3f + p.z * 1.9f) * 0.15f;
return noise;
}
}