Code/Controllers/VortexForce.Cs

Particle controller that applies a vortex force to particles. It computes a cylindrical projection around an axis and accelerates particles tangentially to induce rotation, with configurable center, axis, strength and responsiveness and an option to use local or world space.

Rough CodeNative Interop
using Sandbox;

public class VortexForce() : ParticleController
{
    [Property]
    public Vector3 VortexCenterOffset { get; set; }

    [Property]
    public Vector3 Axis { get; set; } = Vector3.Up;

    /// <summary>
    /// When true the Axis and center offset follow the controller's rotation/position,
    /// so the vortex tilts and moves with the emitter. When false they are world-space.
    /// </summary>
    [Property]
    public bool LocalSpace { get; set; } = false;

    [Property]
    public ParticleFloat Strength { get; set; } = 100f;

    [Property]
    public ParticleFloat Responsiveness { get; set; } = 1f;

    protected override void OnParticleStep(Particle particle, float delta)
    {
        var vortexCenter = LocalSpace
            ? WorldTransform.PointToWorld(VortexCenterOffset)
            : WorldPosition + VortexCenterOffset;
        var axis = LocalSpace ? WorldTransform.NormalToWorld(Axis) : Axis;

        var r = particle.Position - vortexCenter;

        var rPlane = r - Vector3.Dot(r, axis) / Vector3.Dot(axis, axis) * axis;

        if (rPlane.LengthSquared < 0.0001f)
            return;

        var tangent = Vector3.Cross(axis, rPlane.Normal);

        var strength = Strength.Evaluate(delta, 6555f);
        var responsiveness = Responsiveness.Evaluate(delta, 6555f);

        var currentTangentialSpeed = Vector3.Dot(particle.Velocity, tangent);
        var targetDelta = (strength - currentTangentialSpeed) * responsiveness;
        particle.Velocity += tangent * targetDelta * delta;
    }
}