Search Unity

Question Need help tweaking velocity over lifetime module in particle system

Discussion in 'Scripting' started by Crawdad, Mar 10, 2023.

  1. Crawdad

    Crawdad

    Joined:
    Nov 23, 2016
    Posts:
    174
    Hey, guys. I've been trying to find a work-around for this issue for a couple of weeks now, and keep coming up short. The orbital velocity of a particle is always effected by the rotation of the particle transform, even when the space is set to world. So for example, let's say you want to make a gun for your character using the particle system that shoots around the sphere of a planet. This would be super easy using the particle system orbital velocity, except for the fact that as the character rotates, the particles rotate their velocity accordingly, even with space set to world.

    I dove pretty deep working on scripts that manually move particles based on position, but the issues you run into with this is that much of the calculations that the particle system uses for it's modules revolve around velocity, not placement over time, so many things such as trails and stretched billboard effects do not work properly if you are updating position directly.

    Does anyone have a clever hack that gets around this? Obviously the goal is to have the particles continue on their initial orbital velocity even as the particle system transform rotates. Any help appreciated. Hoping devs will chime in on this one since I've found other posts about this same issue with no resolution. Video showing effect of particle transform rotation below. @richardkettlewell.

     
    Last edited: Mar 10, 2023
  2. richardkettlewell

    richardkettlewell

    Unity Technologies

    Joined:
    Sep 9, 2015
    Posts:
    2,284
    The space appears under the linear option to indicate that it only applies to the linear velocity option.

    There is no option to do this for the orbital velocity - it always orbits the transform including its rotation.

    I can think of 3 options:
    1. Submit it as a bug report, maybe I can add an option for orbital velocity space
    2. In script, set the transform rotation to identity, call ps.Simulate, then restore the rotation. It may cause other weird behaviour though…
    3. Use the particle job system integration to write your own update job to orbit the particles in world space
     
    Bunny83 and Crawdad like this.
  3. Crawdad

    Crawdad

    Joined:
    Nov 23, 2016
    Posts:
    174
    Thanks for the response, Richard! I did spend some time on your second suggestion and wasn't able to get anywhere with it. I did look into option 3 as well, but it seems a bit beyond my capabilities, so I ended up creating a bug report. I appreciate that you guys have a lot on your plate, and this may not be high up on the list, but if you are able to find a way to accomplish this, I really feel like it would add a lot of versatility to the particle system. Thanks for all you do!
     
    richardkettlewell likes this.
  4. richardkettlewell

    richardkettlewell

    Unity Technologies

    Joined:
    Sep 9, 2015
    Posts:
    2,284
    Thanks. Do you have a report number?
     
  5. Crawdad

    Crawdad

    Joined:
    Nov 23, 2016
    Posts:
    174
    Not yet. I'll update when I get the email.
     
    richardkettlewell likes this.
  6. Crawdad

    Crawdad

    Joined:
    Nov 23, 2016
    Posts:
    174
    Still didn't have an email this morning so just re-submitted. Case number IN-35239.
     
    richardkettlewell likes this.
  7. richardkettlewell

    richardkettlewell

    Unity Technologies

    Joined:
    Sep 9, 2015
    Posts:
    2,284
    Crawdad likes this.
  8. Crawdad

    Crawdad

    Joined:
    Nov 23, 2016
    Posts:
    174
    Hey, I'm stoked you're working on this! I thought it was just going to go in what I'm sure is an already overwhelming pile of suggestions. I appreciate you taking the time.

    I tried it out, but unfortunately it's not exactly what I was getting at. I probably wasn't doing a very good job of explaining it. It looks like what you've got going now it that when the orbital velocity is set to world space, the initial velocity of all the particles is always in the same direction, even when the transform rotates, but the velocity of the already enabled particles changes as the transform rotates. What I was more looking for is that it would be great if the particles already enabled continue on their original orbital velocity, even if the transform rotation is changed. Hard to describe, but I've got a couple of videos below that will hopefully shed some light.

    This is what you have at the moment. As you can see, as the transform rotates, the particles update their orbital velocity to line up with the transform, even though there initial velocity is always in the same direction. This is with the ParticleSystem.main set to local. When set to world, no rotation is allowed at all.

    Here, I'm updating the position of the particles directly to get the effect I'm looking for. As you can see, the particles continue on their original orbital path created when they are enabled, even when the transform rotates. Unfortunately this approach doesn't work well with any of the modules in the particle system that rely on velocity.

    Hope that helps. I appreciate you taking the time to look at this.
     
    Last edited: Mar 23, 2023
    richardkettlewell likes this.
  9. richardkettlewell

    richardkettlewell

    Unity Technologies

    Joined:
    Sep 9, 2015
    Posts:
    2,284
    I haven't fully digested this yet, i will do that next week :) but, a random idea - could rotating the shape transform in the shape module help here? instead of rotating the game object.
     
    Crawdad likes this.
  10. Crawdad

    Crawdad

    Joined:
    Nov 23, 2016
    Posts:
    174
    So unfortunately the shape doesn't really apply here since I'm not using the it to create the spherical travel, just the starting point of the particles, so the velocity direction isn't effected by the particle spawn shape. Good thought though. I tested it out just to make sure, but no dice. If only it were that easy *sigh*...
     
  11. richardkettlewell

    richardkettlewell

    Unity Technologies

    Joined:
    Sep 9, 2015
    Posts:
    2,284
    Ok, I've read your reply in more detail this morning, and I think it's perhaps a bit specific for us to implement built in to the UI. (because each particle would need to track its original rotation direction, in order to preserve it when the system's rotation changes)

    However, I think you could do this with the job system support for the particle system. I'll see if I can put something together.
     
    Crawdad likes this.
  12. richardkettlewell

    richardkettlewell

    Unity Technologies

    Joined:
    Sep 9, 2015
    Posts:
    2,284
    I've realised, you must have already coded up more-or-less what you want, to be able to share that second video?

    What velocity problems are you facing? And what does your code look like?
     
    Crawdad likes this.
  13. Crawdad

    Crawdad

    Joined:
    Nov 23, 2016
    Posts:
    174
    The problems lie with particle trails and the render modes in the renderer module. The only way I was able to code what I wanted was to manually change the position of the particles over time, which it seems that a lot of the modules in the particle system use velocity, not position in their math. This is especially obvious when you try to use Stretched Billboard in the Render Mode, since it doesn't stretch with the particle movement at all. Particle trails kind of work, but it's buggy as you can see in this vid.

    The script is just kind of a clumsy strong-arm way to get what I wanted. I found the most reliable method was to enable all the particles right off the bat so their id is set in the array, then disable them. Then I create a separate array with Pose so that I can set and reference the initial position and rotation of the transform when the particle is enabled. A good bit of this doesn't apply, but starting at line 181 is where I set the position of the particles.

    EDIT: To clarify, the particle system emission and velocity are set to zero. The code is solely responsible for enabling the particles and moving them.

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using System.Linq;
    5.  
    6. [System.Serializable]
    7. public class ParticleClass
    8. {
    9.     public bool isOccupied;
    10.     public Pose pose;
    11.     public float angle;
    12.     public float startTime;
    13.     public float size;
    14. }
    15.  
    16. public class ParticleDamage : MonoBehaviour
    17. {
    18.     public bool isEnabled, isPersistent, isLaser;
    19.     public float damage, fireRate, speed, stagger, timeAlive, subTimeAlive, radius, damageRate;
    20.     public float collisionRad, damageRad;
    21.     public ParticleSystem ps, subps, muzzleFlash;
    22.     ParticleSystem.Particle[] particles, subParticles;
    23.     public LayerMask colliderMask, damageMask;
    24.     public List<ParticleClass> parts;
    25.     private void Awake()
    26.     {
    27.         clock = 999;
    28.         //transform.localPosition = new Vector3(0, -radius, 0);
    29.         CreatePartsList();
    30.         SetParticles();
    31.     }
    32.  
    33.     private void CreatePartsList()
    34.     {
    35.         if (isLaser) { return; }
    36.  
    37.         parts = new List<ParticleClass>();
    38.         int i = 0;
    39.         while(i < ps.main.maxParticles)
    40.         {
    41.             i++;
    42.             parts.Add(new ParticleClass());
    43.         }
    44.     }
    45.  
    46.     private void FixedUpdate()
    47.     {
    48.         AddParticle();
    49.         Position();
    50.         Damage();
    51.         RemoveOld();
    52.         AgeSubs();
    53.         Lighting();
    54.     }
    55.  
    56.     private void SetParticles()
    57.     {
    58.         if (isLaser) { return; }
    59.  
    60.         //Primary Particles
    61.         particles = new ParticleSystem.Particle[ps.main.maxParticles];
    62.         ps.Emit(ps.main.maxParticles);
    63.         ps.GetParticles(particles);
    64.  
    65.         for (int i = 0; i < particles.Length; i++)
    66.         {
    67.             particles[i].position = Vector3.zero;
    68.             particles[i].remainingLifetime = -1;
    69.             parts[i].size = particles[i].size;
    70.         }
    71.         ps.SetParticles(particles, particles.Length);
    72.  
    73.         //subParticles
    74.         if (subps != null)
    75.         {
    76.             subParticles = new ParticleSystem.Particle[subps.main.maxParticles];
    77.             subps.Emit(subps.main.maxParticles);
    78.             subps.GetParticles(subParticles);
    79.  
    80.             for (int i = 0; i < subParticles.Length; i++)
    81.             {
    82.                 subParticles[i].position = Vector3.zero;
    83.                 subParticles[i].remainingLifetime = -1;
    84.             }
    85.             subps.SetParticles(subParticles, subParticles.Length);
    86.         }
    87.     }
    88.  
    89.     float clock;
    90.     public int numParticles, subNumParticles;
    91.     public bool emit;
    92.     int count;
    93.     void AddParticle()
    94.     {
    95.         clock += Time.deltaTime;
    96.         if (clock < fireRate || !isEnabled) { return; }
    97.         clock = 0;
    98.  
    99.         for (int i = 0; i < parts.Count; i++)
    100.         {
    101.             if (!parts[i].isOccupied)
    102.             {
    103.                 count = i;
    104.                 break;
    105.             }
    106.         }
    107.  
    108.         parts[count].isOccupied = true;
    109.         parts[count].startTime = Time.time;
    110.  
    111.         //set position and rotation for new particle
    112.         parts[count].pose.rotation.eulerAngles = transform.rotation.eulerAngles;
    113.         transform.localEulerAngles = new Vector3(0, 90 + Random.Range(-stagger, stagger), 0);
    114.         parts[count].pose.position = transform.localPosition;
    115.  
    116.  
    117.         particles[count].position = parts[count].pose.position;
    118.         ps.SetParticles(particles, count);  //Needed to keep trail from skipping back to zero mark
    119.  
    120.         particles[count].remainingLifetime = timeAlive;
    121.  
    122.         if (muzzleFlash != null) { muzzleFlash.Emit(1); }
    123.     }
    124.  
    125.     float damageClock;
    126.     void Damage()
    127.     {
    128.         damageClock += Time.deltaTime;
    129.         if (damageClock < damageRate || particles.Length == 0) { return; }
    130.         damageClock = 0;
    131.  
    132.         // iterate through the alive particles for damage
    133.         for (int i = 0; i < particles.Length; i++)
    134.         {
    135.             if (parts[i].isOccupied)
    136.             {
    137.                 var position = particles[i].position;
    138.                 Collider[] hitColliders = Physics.OverlapSphere(position, collisionRad, colliderMask);
    139.                 if (hitColliders.Length > 0)
    140.                 {
    141.                     if (subps != null)
    142.                     {
    143.                         subParticles[i].remainingLifetime = subTimeAlive;
    144.                         subParticles[i].startLifetime = subTimeAlive;
    145.                         subParticles[i].position = position;
    146.                         subps.SetParticles(subParticles, subParticles.Length);
    147.                     }
    148.  
    149.                     hitColliders = Physics.OverlapSphere(position, damageRad, damageMask);
    150.                     foreach (Collider enemy in hitColliders)
    151.                     {
    152.                         enemy.GetComponent<EnemyHealth>().incomingPulseDamage += damage;
    153.                     }
    154.  
    155.                     if (!isPersistent && !isLaser)
    156.                     {
    157.                         particles[i].remainingLifetime = -1;
    158.                     }
    159.                 }
    160.             }
    161.         }
    162.     }
    163.  
    164.     void AgeSubs()
    165.     {
    166.         if (isLaser) { return; }
    167.  
    168.         if (subps != null)
    169.         {
    170.             for (int i = 0; i < subParticles.Length; i++)
    171.             {
    172.                 if (subParticles[i].remainingLifetime >= 0)
    173.                 {
    174.                     subParticles[i].remainingLifetime -= Time.deltaTime;
    175.                 }
    176.             }
    177.             subps.SetParticles(subParticles, subParticles.Length);
    178.         }
    179.     }
    180.  
    181.     void Position() //Moves the particles over a sphere of radius based off initial rotation
    182.     {
    183.         if (isLaser) { return; }
    184.         for (int i = 0;  i < parts.Count; i++)
    185.         {
    186.             if (parts[i].isOccupied)
    187.             {
    188.                 particles[i].remainingLifetime -= Time.deltaTime;
    189.                 parts[i].angle += Time.deltaTime * speed; // update angle
    190.                 Vector3 direction = Quaternion.AngleAxis(parts[i].angle, parts[i].pose.forward) * parts[i].pose.up; // calculate direction from center - rotate the up vector Angle degrees clockwise
    191.                 particles[i].position = (direction * radius) + parts[i].pose.position; // update position based on the direction and the radius
    192.                 if (partSize.keys.Length > 0) { particles[i].size = parts[i].size * partSize.Evaluate(0.01f / particles[i].remainingLifetime); }
    193.             }
    194.         }
    195.         ps.SetParticles(particles, parts.Count);  //This must be included to apply changes to particles
    196.     }
    197.  
    198.     void RemoveOld()
    199.     {
    200.         if (isLaser) { return; }
    201.  
    202.         for (int i = 0; i < particles.Length; i++)
    203.         {
    204.             if (particles[i].remainingLifetime <= 0)
    205.             {
    206.                 ResetPart(i);
    207.                 //particles[i].position = Vector3.zero;
    208.             }
    209.         }
    210.     }
    211.  
    212.     void ResetPart(int index)
    213.     {
    214.         parts[index].isOccupied = false;
    215.         parts[index].pose = new Pose();
    216.         parts[index].angle = 0;
    217.         parts[index].startTime = 0;
    218.     }
    219.  
    220.     public List<LightFlicker> lights;
    221.     void Lighting()
    222.     {
    223.         if (lights.Count == 0) { return; }
    224.         if (isEnabled)
    225.         {
    226.             foreach (LightFlicker light in lights)
    227.             {
    228.                 light.on = true;
    229.             }
    230.         }
    231.         else
    232.         {
    233.             foreach (LightFlicker light in lights)
    234.             {
    235.                 light.on = false;
    236.             }
    237.         }
    238.     }
    239.  
    240.     public AnimationCurve partSize;
    241.     float variablesClock;
    242.     void Variables()
    243.     {
    244.         variablesClock += Time.deltaTime;
    245.         if (variablesClock < 0.25f) { return; }
    246.         variablesClock = 0;
    247.  
    248.         var emission = ps.emission;
    249.         emission.rateOverTime = fireRate;
    250.     }
    251. }
    252.  
    253.  
     
    Last edited: Mar 27, 2023
  14. richardkettlewell

    richardkettlewell

    Unity Technologies

    Joined:
    Sep 9, 2015
    Posts:
    2,284
    Interesting - thanks for sharing.

    Basically, i'm wondering if you can set the velocity in order to get the desired position, instead of setting the position directly.

    This ought to work as long as nothing in the particle system is also modifying the velocity. So I mean something like this:

    Outside the loop:
    Code (CSharp):
    1. float invDt = 1.0f / Time.deltaTime;

    Inside the loop:
    Code (CSharp):
    1. var oldPosition = particles[i].position;
    2. /* maths */
    3. var newPosition = (direction * radius) + parts[i].pose.position;
    4. particles[i].velocity = (newPosition - oldPosition) * invDt;

    Then, the built-in particle update should move your particles to the new position because the velocity is set to the right delta.
     
    Last edited: Mar 29, 2023
    Crawdad likes this.
  15. Crawdad

    Crawdad

    Joined:
    Nov 23, 2016
    Posts:
    174
    I had a very similar thought, but couldn't get the math to work out. It's not exactly my strong point. This seemed closer than my own attempts, but obviously still has some issues. Here's the new codes for position.

    Code (CSharp):
    1.     void Position() //Moves the particles over a sphere of radius based off initial rotation
    2.     {
    3.         if (isLaser) { return; }
    4.  
    5.         float invDt = 1.0f / Time.deltaTime;
    6.         for (int i = 0;  i < parts.Count; i++)
    7.         {
    8.             if (parts[i].isOccupied)
    9.             {
    10.                 var oldPosition = particles[i].position; //Added
    11.  
    12.                 particles[i].remainingLifetime -= Time.deltaTime;
    13.                 parts[i].angle += Time.deltaTime * speed; // update angle
    14.                 Vector3 direction = Quaternion.AngleAxis(parts[i].angle, parts[i].pose.forward) * parts[i].pose.up; // calculate direction from center - rotate the up vector Angle degrees clockwise
    15.  
    16.                 var newPosition = (direction * radius) + parts[i].pose.position; //Added
    17.                 particles[i].velocity = (newPosition - oldPosition) * invDt; //Added
    18.  
    19.                 if (partSize.keys.Length > 0) { particles[i].size = parts[i].size * partSize.Evaluate(0.01f / particles[i].remainingLifetime); }
    20.             }
    21.         }
    22.         ps.SetParticles(particles, parts.Count);  //This must be included to apply changes to particles
    23.     }
    Here's the result. It's almost as if the radius value is fluctuating, but it's not. Not sure what's causing that jitter.
     
  16. richardkettlewell

    richardkettlewell

    Unity Technologies

    Joined:
    Sep 9, 2015
    Posts:
    2,284
    My last ideas: as well as using this new code, also change the Maximum Particle Timestep in the Time Project Settings, to be something very large. To force the internal update to do the whole thing in 1 slice.

    Or, take over the particle simulation too, and call ps.Simulate, with false for the fixedTimeStep parameter, so you can control the simulation yourself too.

    And finally, make sure the velocity isn't left applied for more than 1 frame. with this approach, you'll need to set it every frame. For example when isOccupied==false, do you need to set the velocity to 0?
     
    Crawdad likes this.
  17. Crawdad

    Crawdad

    Joined:
    Nov 23, 2016
    Posts:
    174
    Thanks for the ideas, Richard. Unfortunately, none of these seemed to have any effect at all. I'm stumped. I'll keep fiddling with it, but not I'm not sure what's going on. Also, I noticed when turning on the trails that not all the particles seem to be producing a trail, which leads me to wonder if there isn't something more nefarious wrong with my code. Perhaps the way I'm referencing the particles. Just not sure.
     
  18. smash-ter

    smash-ter

    Joined:
    Sep 6, 2020
    Posts:
    22
    @richardkettlewell I feel that velocity over lifetime relies on the transform values of the game object the particle system is attached to rather than utilizing the particle's own velocity. This is a bigger headache when trying to use a subemitter, as it's basing it's orbit on the game object regardless if you're using local or world space.