Unity Velocity Field Tool

Developed a tool to create a curve that generates a velocity field which will drive/attract game objects around it.


C#, HLSL, Unity


Code : https://github.com/fkkcloud/UnityParticleCurveFollow


1. Unity does not have curve creator so I create bezier/hermite curve creator.

2. Create velocity field and have a mesh follow along with the curve.

3. Translating control point of curve will update the movement along with the curve in real-time.

4. Not just general game objects but particle can use it as well.


using UnityEngine;

[ExecuteInEditMode]
public class VelocityField : BezierCurveBase
{
    [Header("Velocity Field")]
    [Tooltip("Create Velocity Field")]
    public bool CreateVelocityField = true;

    [Tooltip("Update Velocity Field every frame")]
    public bool UpdateEveryFrame = false;

    [Tooltip("Visualize the radius that the velocity field will affect")]
    public bool ShowSearchRadius = true;

    [Tooltip("Raidus - the size of the radius that the velocity field will affect")]
    [Range(0.1f, 100)]
    public float SearchRadius = 5f;

    [Tooltip("Map the magnitude of curve velocity")]
    public AnimationCurve VelocityMagCurve = AnimationCurve.EaseInOut(0f, 0.8f, 1f, 1.0f);

    public bool Visualize = true;
    public float DisplayVelocityLength = 12f;

    protected override void Start()
    {
        base.Start();

        if (CreateVelocityField)
        {
            CalculateVelocityField();
        }
    }

    protected override void Update()
    {
        base.Update();

#if UNITY_EDITOR
        CalculateVelocityField(); // just run calculating the velocity field everytime we iterate on editor..
#endif
        if (UpdateEveryFrame)
        {
            CalculateVelocityField();
        }
    }

    public void CalculateVelocityField()
    {
        VelocityField.Clear();

        Vector3 prevPos = P0.transform.position;
        for (int c = 1; c <= Resolution; c++)
        {
            float t = (float)c / Resolution;
            Vector3 currPos = CurveMath.CalculateBezierPoint(t, P0.transform.position, P0_Tangent.transform.position, P1_Tangent.transform.position, P1.transform.position);
            Vector3 currTan = (currPos - prevPos).normalized;
            float mag = VelocityMagCurve.Evaluate(t);

            VelocityFieldNode ti = new VelocityFieldNode();
            ti.TargetPosition = prevPos;
            ti.TargetVelocity = currTan;
            ti.Mag = mag;
            VelocityField.Add(ti);
            prevPos = currPos;
        }
    }

    override public void OnDrawGizmos()
    {
        base.OnDrawGizmos();

        if (ShowSearchRadius)
        {
            Gizmos.color = GizmoColor;
            Gizmos.DrawWireSphere(P0.transform.position, SearchRadius);
            Gizmos.DrawWireSphere(P1.transform.position, SearchRadius);
        }

        if (Visualize)
        {
#if UNITY_EDITOR
            CalculateVelocityField();
#endif
            float MaxMag = float.MinValue;
            float MinMag = float.MaxValue;

            for (int i = 0; i < VelocityField.Count; i++)
            {
                if (VelocityField[i].Mag > MaxMag)
                {
                    MaxMag = VelocityField[i].Mag;
                }
                if (VelocityField[i].Mag < MinMag)
                {
                    MinMag = VelocityField[i].Mag;
                }
            }

            for (int i = 1; i < VelocityField.Count; i++)
            {
                float color = Remap(VelocityField[i - 1].Mag, MinMag, MaxMag, 0.05f, 1f);

                Color colorShift = new Color(GizmoColor.r * color, GizmoColor.g * color, GizmoColor.b * color);

                Gizmos.color = colorShift;
                Vector3 direction = transform.TransformDirection((VelocityField[i].TargetPosition - VelocityField[i - 1].TargetPosition).normalized) * DisplayVelocityLength;
                Gizmos.DrawRay(VelocityField[i - 1].TargetPosition, direction);
            }
        }
    }
}

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[ExecuteInEditMode]
public class ParticleCurveFollow : MonoBehaviour {


    [Header("VelocitySource")]
    [Tooltip("Use Bezier Curve to drive particle's position and velocity")]
    public VelocityField Curve;

    [Header("Particle Control")]
    [Tooltip("How fast the particle will move along the curve")]
    public float SpeedOnCurve = 1f;
    [Tooltip("How fast the particle will move to the curve")]
    public float ForceToNearestCurve = 0f;

    private ParticleSystem ParticleSys;
    private float SearchRadius = 5f;

    // Use this for initialization
    void Start ()
    {
        ParticleSys = GetComponent<ParticleSystem>();

        if (!ParticleSys)
        {
            Debug.LogError("There is no ParticleSystem component in this gameObject.");
            return;
        }

        // set the particle systems that particle surf is using set to simulation space to be world
        ParticleSystem.MainModule main = ParticleSys.main;
        if (main.simulationSpace != ParticleSystemSimulationSpace.World)
            main.simulationSpace = ParticleSystemSimulationSpace.World;

        SetupCurveData();
    }

    void Update() {

        if (!ParticleSys)
        {
            return;
        }

#if UNITY_EDITOR
        if (Curve)
        {
            Curve.CalculateVelocityField();
        }
#endif
        UpdateParticles();
    }

    public void SetupCurveData()
    {
        if (!Curve)
            return;

        SearchRadius = Curve.SearchRadius;
    }

    void UpdateParticles()
    {
        if (!ParticleSys)
            return;

        ParticleSystem.Particle[] ParticleList = new ParticleSystem.Particle[ParticleSys.particleCount];
        int NumParticleAlive = ParticleSys.GetParticles(ParticleList);
        for (int i = 0; i < NumParticleAlive; ++i)
        {
            ParticleSystem.Particle Particle = ParticleList[i];

            VelocityFieldNode velocityField = new VelocityFieldNode();

            bool IsImported = false;
            if (Curve)
            {
                IsImported = GetTargetNode(Particle, ref Curve.VelocityField, ref velocityField);
            }

            if (!IsImported)
                continue;

            Vector3 targetVelocity = velocityField.TargetVelocity * SpeedOnCurve * velocityField.Mag;

            // get vector from particle position to curve's iteration pos
            Vector3 toCurveVelocity = (velocityField.TargetPosition - Particle.position).normalized;
            targetVelocity += toCurveVelocity * ForceToNearestCurve;

            // apply hierarchy scale for velocity as well
            targetVelocity.x *= transform.lossyScale.x;
            targetVelocity.y *= transform.lossyScale.y;
            targetVelocity.z *= transform.lossyScale.z;

            ParticleList[i].position = Particle.position + (targetVelocity * Time.deltaTime);
            ParticleList[i].velocity = Particle.velocity;
        }
        ParticleSys.SetParticles(ParticleList, ParticleSys.particleCount);
    }

    private bool GetTargetNode(ParticleSystem.Particle Particle, ref List<VelocityFieldNode> velocityField, ref VelocityFieldNode targetInfo)
    {
        float minDist = float.MaxValue;
        VelocityFieldNode node = new VelocityFieldNode();
        for (int i = 0; i < velocityField.Count; i++)
        {
            float dist = Vector3.Distance(velocityField[i].TargetPosition, Particle.position);

            if (dist > SearchRadius)
                continue;
            
            if (dist < minDist)
            {
                minDist = dist;
                node = velocityField[i];
            }
        }

        targetInfo = node;
        return true;
    }
}