Search Unity

Is it possible to - Trigger C# Events from Graph?

Discussion in 'Visual Effect Graph' started by KingKRoecks, Oct 12, 2020.

  1. KingKRoecks

    KingKRoecks

    Joined:
    Jul 28, 2013
    Posts:
    85
    I've created a cool looking projectile in a Visual Effect Graph.


    Now, can I animate the projectile and generate a "Hit" event to communicate outside of VFX Graph to C#?

    It doesn't seem too terribly complicated, since I'm not actually driving real information out of the particle, but I would like an event when the particle says it's complete since the timings are driven by velocity events.

    Since the trigger event is missing from the block search, I cannot verify that there is anything there that can satisfy this need.
     
  2. KingKRoecks

    KingKRoecks

    Joined:
    Jul 28, 2013
    Posts:
    85
    I would understand if the answer is no for culling reasons.

    I may have to change my strategy and drive the particle completely in C#, it just seems like a waste to have to abandon all of this because there's no way to propagate the success event.
     
  3. VladVNeykov

    VladVNeykov

    Unity Technologies

    Joined:
    Sep 16, 2016
    Posts:
    444
    Hi @KingKRoecks ,

    We are adding in the 10.x package coming out with Unity 2020.2 CPU events which are derived from Spawner contexts.
    These might or might not work for you, as they can be triggered when a new spawn event happens, but not after that (for example, you will not be able to trigger them once the particle dies).

    If you know that the particle will hit the target in, say, 1 second after it was born, you could use CPU events to catch via a delegate in C# when the spawn event was triggered (i.e. the particle was born) and then do your hit logic 1 second later.

    Otherwise, yes, you should drive the particle logic from C# so you have access to its position and detect the hit yourself.
     
    KingKRoecks likes this.
  4. KingKRoecks

    KingKRoecks

    Joined:
    Jul 28, 2013
    Posts:
    85
    I think that would work. I'm a bit unsure of the difference between CPU and GPU event contexts right now.

    Theoretically, with the combination of the "Trigger Event on Die" causing the spawn of another particle, you could trigger the C# event in the second particle's spawn context effectively passing the first particle's death out to code.

    Ultimately, having a magic system driven by particles that can be culled from camera is probably not an appropriate implementation. I was planning on mounting the system to the camera, but it still feels like a fragile implementation the more thought I put into it.
     
  5. KingKRoecks

    KingKRoecks

    Joined:
    Jul 28, 2013
    Posts:
    85
    Out of curiosity, is it better to create a new VFXGraph per particle instance or to have a shared graph and drive instances by spawn events?

    The a VFXGraph instance per particles seems to give more control over per-particle positions since you wouldn't need to worry about sharing Vector3 properties.
    The latter seems like it would be more memory optimized, but you end up limited by the accessible attributes on a per-particle basis. You can leverage the targetPosition attribute to store a destination, but the source position is either the local 0,0,0 of the system or a system-wide Vec3 property. Trying to put arbitrary data into unused properties is how I'm currently running a state machine in the VFXGraph, but I don't know if attributes like "direction" can be changed outside of my code or are normalized to where their usefulness is limited.
     
  6. VladVNeykov

    VladVNeykov

    Unity Technologies

    Joined:
    Sep 16, 2016
    Posts:
    444
    GPU Events are used in conjunction with triggers blocks in Update to spawn particles from another system and inherit its attributes. They works a bit like sub emitters from the Particle System in Unity.

    CPU Output Events can only be connected to Spawners, so you won't be able to use them alongside GPU events. Output Events are used to synchronize spawn events with things on the CPU, for example to spawn prefabs with lights whenever/wherever you randomly spawn particles:


    Typically it's better to reuse the same graph and use events to send additional data.

    I have good news for you :) Check out Event Attributes as the bottom of the manual; alongside events, you can send event attributes, something like this (pseudo-code):

    Code (CSharp):
    1. vfx = GetComponent<VisualEffect>();
    2. eventAttribute = vfx.CreateVFXEventAttribute();
    3. eventAttribute.SetVector3("position", target.position);
    4. vfx.SendEvent("Boom", eventAttribute);
    This will not only send a Boom event to the graph, but it will also set the "position" attribute to the value you want.
    Then all you have to do is in Initialize to use an Inherit Position block to get the position passed on via the event to the spawner.

    Using this method you can have 1 VFX which controls pretty much everything, for example projectiles, impact, etc. The only thing to watch out for is the culling, so you have to either make sure the effect's bounding box is always seen.
     

    Attached Files:

  7. KingKRoecks

    KingKRoecks

    Joined:
    Jul 28, 2013
    Posts:
    85
    That's awesome! I was pretty sure there was some way to send attributes in via code, but I hadn't found any concrete examples.

    Apparently in the older revisions (6.x?) there used to be Custom Attributes. Are these still a possibility or are you limited to the predefined attributes that have corresponding "getters"?
     
  8. KingKRoecks

    KingKRoecks

    Joined:
    Jul 28, 2013
    Posts:
    85
    Oh, and one last thing - re:attributes;

    Since I've basically decided that I need to have some function outside of the VFX actually driving the collision detection of a projectile, I imagine that this would make it impossible to send a "Current Position" to the particle, since the attribute would only be accessible at spawn of the particle.

    This would mean that I can only REALLY drive the variables at instantiation of the particle. Anything other than that, and I'm sharing properties.

    So in effect, this means that whatever I pass in has to be a static value and cannot be a moving object.

    That being the case, what would you recommend for this situation? I imagine I can use a relative (local coordinate 0,0,0 instead of world) point for the source and have that be static, but I'd really like to have a dynamic target point.

    In my gif above, I've used a Property Binder to bind a target gameobject to a Vector3 TargetPosition (not to be confused with targetPosition)
     
    Last edited: Oct 13, 2020
  9. VladVNeykov

    VladVNeykov

    Unity Technologies

    Joined:
    Sep 16, 2016
    Posts:
    444
    We still have custom attributes, but if I recall correctly, you can't send them via even attributes and they can't be inherited. Still, you can temporarily use any other built-in attribute really and then re-assign it to your custom attribute in Initialize:


    If you need to feed and update multiple positions at the same time, you can bake them into a texture2D (similar to what the multiple position binder does, RGB translate to XYZ coordinates) and keep updating it. Then you can use something like the particle ID to determine which pixel in the texture (i.e. position coordinates) it needs to read. You can even do something cheeky and store in the alpha channel whether the particle is alive (i.e. alpha 1 == alive, alpha 0 == dead) to selectively destroy particles which have hit their target.

    (*make sure your divide is set to float, otherwise it will not return a fraction)
     

    Attached Files:

  10. KingKRoecks

    KingKRoecks

    Joined:
    Jul 28, 2013
    Posts:
    85
    That's fantastic!

    Almost there then. The one difficulty still remaining would be the control of the particle ID and trying to line that up with the pixel index.

    I just did a quick test and it seems like the "particleId" increments forever, never looping back to origin until a recompile.

    So that means that I'll need to store the last seen integer inside of code as well. When my "maxParticleCount" wraps around, I need to modulo that number in both code and particle when referring to the texture coord.

    There doesn't appear to be any return signature from the event, so I won't be able to actually access the particleId and I'll just need to hope they stay aligned. Since I'd be driving a trail behind it, it would be really unfortunate for the particle ID and the pixel index to every be out of sync, since that would cause a really jerky effect with the trail renderer.
     
  11. VladVNeykov

    VladVNeykov

    Unity Technologies

    Joined:
    Sep 16, 2016
    Posts:
    444
    Just use a modulo operator to keep it within a certain range:


    Alternatively, you can send your own custom "particle ID" with the event attribute (maybe store it in texIndex or something else you are not using), this way you can in C# recycle the particle IDs (i.e. have an array or list of 10 projectiles, and projectile 5 expires, you can then send a new event with the ID 5 and a new target position on the texture for it to track.)
     

    Attached Files:

  12. KingKRoecks

    KingKRoecks

    Joined:
    Jul 28, 2013
    Posts:
    85
    As far as I can tell, this doesn't appear to be working:

    Code (CSharp):
    1. var eventAttribute = ProjectileEffect.CreateVFXEventAttribute();
    2.         eventAttribute.SetVector3("direction", new Vector3(0, lastProjectileId, 0));
    3.         ProjectileEffect.SendEvent("Fire", eventAttribute);
    I've hooked up the graph to leverage the passed in "direction" and inherit it:
    upload_2020-10-13_1-49-59.png

    I know I'm close, but right now the particle does not appear to be moving at all.

    I'm updating a COLOR[256] with a new Color(x, y, z, 1) every frame.

    Code (CSharp):
    1. ...
    2. COLOR_BUFFER[i] = new Color(newPos.x,
    3.                     newPos.y,
    4.                     newPos.z,
    5.                     1);
    6.  
    7. ...
    8.  
    9. pointCache.SetPixels(COLOR_BUFFER);
    10. VFX.SetTexture("PointCache", pointCache);
    If you can see anything obviously wrong with what I'm doing, I'd love to fix this. I'm so close haha
     
  13. VladVNeykov

    VladVNeykov

    Unity Technologies

    Joined:
    Sep 16, 2016
    Posts:
    444
    Is your texture vertical? You seem to be sampling the Y of the texture, not the X.
     
  14. KingKRoecks

    KingKRoecks

    Joined:
    Jul 28, 2013
    Posts:
    85
    Code (CSharp):
    1. using System;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using Sirenix.OdinInspector;
    5. using UnityEngine;
    6. using UnityEngine.VFX;
    7.  
    8. [Serializable]
    9. public class ProjectileTracker
    10. {
    11.     public int projectileId;
    12.     public Vector3 lastPosition;
    13.     public Transform trackedObject;
    14. }
    15.  
    16. [ExecuteAlways]
    17. public class ParticleProjectileDriver : MonoBehaviour
    18. {
    19.     public Dictionary<int, ProjectileTracker> ProjectileTrackers = new Dictionary<int, ProjectileTracker>();
    20.  
    21.     public int activeParticleCount;
    22.     public int lastProjectileId = 0;
    23.    
    24.     public VisualEffect ProjectileEffect;
    25.  
    26.     public Texture2D pointCache;
    27.    
    28.     // Start is called before the first frame update
    29.     void Start()
    30.     {
    31.        
    32.     }
    33.  
    34.     // Update is called once per frame
    35.     void Update()
    36.     {
    37.         UpdateTrackers();  
    38.     }
    39.  
    40.     public GameObject TestTarget;
    41.    
    42.     [Button]
    43.     public void SpawnProjectile()
    44.     {
    45.     SpawnProjectile(TestTarget.transform);
    46.     }
    47.  
    48.     public void SpawnProjectile(Vector3 targetPosition)
    49.     {
    50.        
    51.     }
    52.  
    53.     [Button]
    54.     public void ClearProjectiles()
    55.     {
    56.         lastProjectileId = -1;
    57.         ProjectileTrackers = new Dictionary<int, ProjectileTracker>();
    58.         ProjectileEffect.Stop();
    59.         ProjectileEffect.Play();
    60.     }
    61.  
    62.    
    63.  
    64.     private Color[] COLOR_BUFFER = new Color[256];
    65.     private Color EMPTY = new Color(0, 0, 0, 0);
    66.     public float TravelSpeed = 1f;
    67.    
    68.     public void SpawnProjectile(Transform trackingTransform)
    69.     {
    70.         ProjectileTracker tracker = new ProjectileTracker();
    71.         tracker.trackedObject = trackingTransform;
    72.         lastProjectileId++;
    73.         lastProjectileId %= 256;
    74.  
    75.         tracker.lastPosition = transform.position;
    76.         tracker.projectileId = lastProjectileId;
    77.         ProjectileTrackers.Add(tracker.projectileId, tracker);
    78.        
    79.         var eventAttribute = ProjectileEffect.CreateVFXEventAttribute();
    80.         eventAttribute.SetVector3("direction", new Vector3(0, lastProjectileId, 0));
    81.         ProjectileEffect.SendEvent("Fire", eventAttribute);
    82.     }
    83.    
    84.     public void UpdateTrackers()
    85.     {
    86.         if (pointCache == null || pointCache.width != 256)
    87.         {
    88.             pointCache = new Texture2D(256, 1, TextureFormat.RGBA32, false, true);
    89.             pointCache.filterMode = FilterMode.Point;
    90.         }
    91.        
    92.         Queue<int> cleanup = new Queue<int>();
    93.         for (int i = 0; i < 256; i++)
    94.         {
    95.             if (ProjectileTrackers.TryGetValue(i, out var found))
    96.             {
    97.                 var foundTracker = found.trackedObject;
    98.                 var diffVec = foundTracker.position - found.lastPosition;
    99.                 var newPos = Vector3.Lerp(found.lastPosition, foundTracker.position, Time.deltaTime * TravelSpeed);
    100.                 var distance = Vector3.Distance(newPos, foundTracker.position);
    101.                 Debug.LogFormat("Moving Projectile {0} / {3} - Old Pos {1} - New Pos {2}", found.projectileId,
    102.                     found.lastPosition, newPos, distance);
    103.                 found.lastPosition = newPos;
    104.  
    105.                
    106.                 if (distance <= 0.01f)
    107.                 {
    108.                     cleanup.Enqueue(i);
    109.                 }
    110.                
    111.                 COLOR_BUFFER[i] = new Color(newPos.x,
    112.                     newPos.y,
    113.                     newPos.z,
    114.                     1);
    115.             }
    116.             else
    117.             {
    118.                 COLOR_BUFFER[i] = EMPTY;
    119.             }
    120.  
    121.             while (cleanup.Count > 0)
    122.             {
    123.                 ProjectileTrackers.Remove(cleanup.Dequeue());
    124.             }
    125.         }
    126.        
    127.         pointCache.SetPixels(COLOR_BUFFER);
    128.         ProjectileEffect.SetTexture("PointCache", pointCache);
    129.     }
    130. }
    131.  
     
  15. KingKRoecks

    KingKRoecks

    Joined:
    Jul 28, 2013
    Posts:
    85
    I've tried both X and Y, changing the dimensions of the Texture in Width and Height to correspond. I wasn't sure if it mattered, so I tried both.

    The code is what I'm currently using, and referencing the X dimension as well

    upload_2020-10-13_2-1-29.png
     
  16. KingKRoecks

    KingKRoecks

    Joined:
    Jul 28, 2013
    Posts:
    85
    Is it possible the direction is being normalized to a (0-1) value in the Y dimension?
     
  17. KingKRoecks

    KingKRoecks

    Joined:
    Jul 28, 2013
    Posts:
    85
    Doesn't seem to be the case.
    Code (CSharp):
    1. eventAttribute.SetVector3("targetPosition", new Vector3(0, lastProjectileId, 0));
    Setting this in the attribute instead of direction and inheriting source "targetPosition" and feeding it into the texture and it still just sits at 0,0,0.
     
  18. VladVNeykov

    VladVNeykov

    Unity Technologies

    Joined:
    Sep 16, 2016
    Posts:
    444
    Should be along the X. Try changing your texture format from TextureFormat.RGBA32 to TextureFormat.RGBAFloat; you are storing position which would need more bits per channel.

    What part exactly doesn't work; the texture bit not getting the right positions, or the inheriting of event attributes?
     
  19. KingKRoecks

    KingKRoecks

    Joined:
    Jul 28, 2013
    Posts:
    85
    I'm really not quite sure. I don't really know of any solid way to debug the VFXGraph, since I've only started playing with it over the last week.

    I can't really tell what part isn't working, and they're both fairly new concepts to me if I'm being honest.

    Right now, I'd like to know what the direction is, what index is being fed into the texture, and what X/Y/Z I'm getting back from the SampleTexture node.

    Previously, I was seeing a particle spawn at 0,0,0 and grow in size with the size over life.
    After changing the Texture to a RGBAFloat, I don't see the particle at 0,0,0 anymore and I'm not sure where it is. I had a thought that it might have spawned at 255 for being a white pixel, but no luck.
     
  20. VladVNeykov

    VladVNeykov

    Unity Technologies

    Joined:
    Sep 16, 2016
    Posts:
    444
    Debugging the VFX Graph is similar to debugging any shaders; you can't write of course things to the console, so you resort to visual debuging. The simplest way is typically to just output a color. If you need something a bit more exact, attached is a texture I made for debugging specific values. You can then change your output to use flipbooks and set the texIndex attribute which flipbooks use to determine which tile from the texture to show:
     

    Attached Files:

  21. KingKRoecks

    KingKRoecks

    Joined:
    Jul 28, 2013
    Posts:
    85
    That is a fantastic debug strategy! Thank you for sharing.

    I hooked it up so far like this:
    upload_2020-10-13_17-32-57.png

    The index is shown just to the left of the top, and then X,Y,Z are shown stacked.

    The results are inconclusive lol
    upload_2020-10-13_17-33-53.png
     
  22. KingKRoecks

    KingKRoecks

    Joined:
    Jul 28, 2013
    Posts:
    85
    I can see that I'm setting some of these values to negative, so that could explain weird results. For the others, I'm not sure.

    Code (CSharp):
    1. Moving Projectile ID 0 / DistanceRemaining 10.2555 - Old Pos (-4.8, 0.0, -0.9) - New Pos (-4.7, 0.1, -0.9)
    Performing a little clamping, I'm able to get a 0 to show, but I don't have any faith in this value being correct.

    upload_2020-10-13_17-42-32.png

    On the off chance I set the wrong index, I'm setting the entire array to the point right now:

    upload_2020-10-13_18-9-45.png
     
  23. KingKRoecks

    KingKRoecks

    Joined:
    Jul 28, 2013
    Posts:
    85
    I added an "Output Particle Line" and had it draw a line to the point it thought was the target position from 0,0,0:

    upload_2020-10-13_21-11-57.png

    So it's definitely off by quite a bit, all in the negative dimension too.
     
  24. KingKRoecks

    KingKRoecks

    Joined:
    Jul 28, 2013
    Posts:
    85
    Seems like negative infinity, since no matter how much I scroll, I can't find the terminating point:

    upload_2020-10-13_21-16-15.png
     
  25. KingKRoecks

    KingKRoecks

    Joined:
    Jul 28, 2013
    Posts:
    85
    VladVNeykov likes this.
  26. KingKRoecks

    KingKRoecks

    Joined:
    Jul 28, 2013
    Posts:
    85


    Finished product from this thread (minus any interesting visuals, of course)
     
  27. VladVNeykov

    VladVNeykov

    Unity Technologies

    Joined:
    Sep 16, 2016
    Posts:
    444
    Awesome, really happy to hear you figured it out, @KingKRoecks ! :)
     
  28. JLM_fell

    JLM_fell

    Joined:
    Jul 26, 2018
    Posts:
    6
    I want to see the result, but I can't see the picture
     
unityunity