Search Unity

Question Texture2DArray Node Index Input

Discussion in 'Shader Graph' started by impman, May 13, 2019.

  1. impman

    impman

    Joined:
    Jun 17, 2016
    Posts:
    7
    Hello everyone.

    TL/DR: Why does the noise get passed from the Random Range Node to the Texture2DArray Index Input to the RGBA Output?

    I've setup a ripple shader, that takes in an impact position and over time increases a point/circle in size to then fade out.

    In order to have multiple impacts, I am creating a Texture2DArray in code, and converting these impact positions to colors and applying them to a single pixel texture.
    Had to set the texture format to RGBAFloat in order to have negative numbers and numbers greater than 1.
    This works surprisingly well.

    Would be nice to pass in custom data or a Vector4 array instead though.

    For my initial example I have 16 slices in this Texture2DArray, enabling up to 16 unique ripples at a time.

    The issue I am experiencing is when cycling through the indicies of the array node in order for the effect to grab and available impact positions.

    I need to constantly reference the textures as I am using the alpha channel to increment the size of the ripple. This is set in code, and the texture array is updated every frame.

    If I cycle the index in code, it is always too slow and you see what you would think is frame drop / lag.

    So I tried to be clever, and used the Random Range node, using 0 and 15 as my min and max values.
    I have connected a simple noise node to the seed input of the random range node, and then connected the random range output to the array index input.

    This works to the point that I see the unique ripples draw in real time, however, the noise is passed onto the texture which ruins the visual effect.

    I am unsure if this is how it is suppose to operate or a bug.

    For testing, I tried assigning 0.5 and 0.99 to the index, to see if there is any differences on the visual effect. It seems the index gets rounded to the closest int and this made no change to how it should look.

    I've also tried reducing the strength of the noise, which doesnt really help, you can set it so low you get lines instead of dots, but still, not what i'm going for.

    So it seems that the noise node seeps/leaks through to the output by itself, or the output is configured to always grab available noise...

    To me, as I am purely feeding into the index input, there should be 0 affect on the output texture and just assigning which slice to draw at any given time.

    For context, this is being used as a force field shader that is surrounding a space ship. It isn't visible until there is an impact and only the active ripple(s) are made visible.
    The noise being passed through, is making a lot of it not render due to the transparancy settings, so it ends up looking like a load of particles in a ring. Which in itself is pretty cool, but not the effect i'm going for.

    Furthermore, if anyone has any other alternatives to cycling through all available indicies, I'd be happy to give them a try.

    Apologies for the long post, thank you for reading.

    Steve
     
    Last edited: May 14, 2019
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,348
    ...

    Could you perhaps post some example images of the Shader Graph and the output?
     
  3. impman

    impman

    Joined:
    Jun 17, 2016
    Posts:
    7
    Yes, sorry was at work when I made the post.

    Ok have added the following images:

    How the ripple looks when the Index is set to 0. Showing a single impact.
    ForceFieldShader_Clean_Output.PNG
    How it looks when the noise is used as a seed into the random range node to set the Index.
    ForceFieldShader_Noise_Output.PNG
    This is the setup for the noise as the seed for the random range node that should only output 0-15 into the Index of the Texture2DArray node.
    ForceFieldShader_Noise_T2A.PNG
    This is the rest of the shader that generates the ripple and applies the hex texture to it.
    ForceFieldShader_Ripple.PNG
    This is showing multiple impacts working, but with the noise issue.
    ForceFieldShader_Ripple_Nosie_Multiple_Impacts.PNG
     
  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,348
    Ah, I understand your issue. Everything is working exactly like it should, it’s a problem of what your expectations are.

    The Random Range node outputs a single value per pixel between the min and max values, not multiple values. So for one pixel it’s the equivalent of using 0, for another it’s 6, for another 4, etc. Each pixel is only sampling the texture once and getting the one value from one index.

    To do what you want you need to actually duplicate most of the graph 16 times with the index values manually set for each layer and add together the results.
     
  5. impman

    impman

    Joined:
    Jun 17, 2016
    Posts:
    7
    Hmm, not sure I fully understand.

    Way I see it, the noise node is passing different values into the Random Range seed (constantly?), these values let the random range pick one of the numbers between the min and max defined.
    That goes into the index of the texture array to assign which slice (position) the shader should be using at that given time to generate a ripple.

    The second image is showing what happens when I click on the collider once, generating 1 impact ripple. The last image shows what happens when I click multiple times, to generate multiple impacts.

    For me, the multiple impact logic is fine, and the issue is that the noise for some reason is being passed through to the rest of the shader graph when it shouldn't.

    Or perhaps it should, and thats where my misunderstanding lies..

    From my testing, I feel all I need for this to work with multiple impacts, with the same visual as the first image, is a way to cycle/loop through the texture array available slices.

    One of my tests used an exposed parameter as the texture array index, and I looped through 0-15 once per frame, and the multiple impacts worked, rendered the way I expect (like the 1st image), but the ripples didn't move smoothly, the index updating didn't seem to be happening fast enough.

    example:
    WIP_Shield_VFX_Stutter.gif

    Using the noise and random range, smooths it out but adds the noise to the visual:
    WIP_Shield_VFX_Noise.gif

    Also may help in your understanding of how this is operating to know, that the alpha channel represents how big the ripple is, so when it switches slice, and the texture array has been updated, the alpha value will have changed (as that is handled in code), so it will draw the ripple based on that.

    like so:

    Code (CSharp):
    1. void Update()
    2. {
    3.     for (int j = 0; j < impactPositions.depth; j++)
    4.     {
    5.         var pixels = impactPositions.GetPixels(j);
    6.         if (pixels[0].r != 0 || pixels[0].g != 0 || pixels[0].b != 0)
    7.         {
    8.             pixels[0].a += Time.deltaTime * speed;
    9.  
    10.         }
    11.  
    12.         impactPositions.SetPixels(pixels, j, 0);
    13.         impactPositions.Apply();
    14.         UpdateShaderIndex();
    15.     }
    16. }
     
  6. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,348
    If the game is running at 60 fps, each ring is only going to be visible 4 times a second for that 1/60th of a second that frame is displayed. You only ever see one ring at a time.

    If you're rendering at 300 fps, your monitor is likely only displaying 60 fps, which means 4/5ths of the rendered frames are just being thrown away and never seen. It's not blending those frames together, it's only showing the most recently rendered frame. You still only see one ring at a time.

    Like I said, each pixel is simply showing a single index.

    The Random Range node is a pseudo number generator: Feed in a value and it'll spit out a consistent, but seemingly random value.
    https://docs.unity3d.com/Packages/com.unity.shadergraph@5.3/manual/Random-Range-Node.html

    So some pixels are showing only the results of index 0, another only the results of index 1, etc. This means over the entire surface you're seeing a little bit of each ring on those pixels that accessed that ring's layer index. Like I said before, you have to actually, explicitly, sample all 16 layers in the node graph for this to work like I think you're wanting it to.


    Also, using a Simple Noise node as the input to the Random Range node is unnecessary. Simple Noise is a smooth value noise node, which means it's noise that smoothly interpolates between pseudo random values across it's input. They actually use the same (ubiquitous, but kind of terrible) pseudo random calculation, and if scaled enough the Simple Noise will produce very similar results to the Random Range node when using a 0.0 to 1.0 range, but several times more expensive. However just using UVs as the Seed input on the Random Range node will produce functionally indistinguishable results without the added cost of the Simple Noise node.
     
  7. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,348
    The main problem is Shader Graph doesn't have any ability to iterate or loop over a list of values. Shaders are absolutely capable of doing so, and it's trivial to implement in a hand written shader, just Shader Graph itself doesn't have that option. Hence why to do this you have to duplicate the graph 16 times.

    Really, you just need to have 16 copies of the Sample Texture 2D Array node, all sampling from the same texture property node, and doing the distance -> ring calculations. After that get the max of the 16 rings and feed it into the rest of the shader.
     
  8. impman

    impman

    Joined:
    Jun 17, 2016
    Posts:
    7
    Hmmm ok I see what you're saying now.
    I replaced the noise with a UV node into the random range, and still saw all the noise as before.
    I was wrongfully blaming the noise node for that effect.

    I think I will have to wait for iterations/looping to be possible, as I need this solution to be scale-able to the amount of slices in the array without all that duplicating... for example, a battle-cruiser ship that would need 100 or more max impacts.

    Thank you for working through this with me and helping me understand where the true limitations were, I really appreciate your time and help.
     
  9. alexandral_unity

    alexandral_unity

    Unity Technologies

    Joined:
    Jun 18, 2018
    Posts:
    163
    Loops can function inside of the custom function node available in most recent versions of the package. You can write a iterative loop and set the output and you should be able to get similar results as you would in a handwritten shader. It's just that loops aren't inherent in their own node UX within the graph. What you're trying to achieve should be possible with a little HLSL inside of a custom function node, or a referenced HLSL include file in your graph.
     
  10. impman

    impman

    Joined:
    Jun 17, 2016
    Posts:
    7
    Hi Alexandral,
    Thanks for the tip, I'll have a look into that tomorrow :)
     
  11. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,348
    FYI, while this is something they've mentioned internally discussing, I wouldn't expect it to ever actually get officially added. It's quite rare to see in node based material systems for various reasons, one being it's really hard to properly represent in a node graph in a way that makes sense (for non coders), and because it's really easy to make shaders insanely expensive when you suddenly have a way to do something 1000 times. The Unreal Engine's node base material system is 15 years old at this point and this is a feature they've explicitly avoided adding.

    However, it is still possible. If you have the latest LWRP & 2019.1 you can implement the feature using a Custom Function node (which @alexandral_unity beat me to mentioning).

    I might also suggest skipping using a texture array here and just pass in a regular 2D texture that's 1 pixel high and the max number of impacts wide. There's often some hidden memory overhead to using a texture array, though a 1 pixel texture probably won't make that overhead noticeable.

    In the Custom Function node you'd pass in the texture property, a max count, and then use something like this to actually sample the texture:

    Code (csharp):
    1. // Inputs
    2. // Texture2D : PositionAndRadius
    3. // Vector1 : Count
    4. // Vector3 : WorldPos
    5. // Vector1 : RippleRadius
    6. // Vector1 : RippleWidth
    7. // Vector1 : RippleMaxRadius
    8. // Output
    9. // Vector1 : Out
    10.  
    11. for (int i=0; i<Count; i++)
    12. {
    13.   float4 posAndRadius = LOAD_TEXTURE2D(PositionAndRadius, int2(0, i));
    14.   float ripple = length(worldPos - posAndRadius.xyz) - posAndRadius.w * RippleRadius;
    15.   ripple = cos(clamp(ripple * 3.1415 / RippleWidth, -3.1415, 3.1415));
    16.   Out = max(Out, ripple);
    17. }
    18.  
    19. Out *= 1 - (RippleRadius / RippleMaxRadius);
     
    alexandral_unity likes this.
  12. impman

    impman

    Joined:
    Jun 17, 2016
    Posts:
    7
    I am using HDRP and 2019.1, so will looking into this after work.

    I did however try a single texture with multiple pixels, and while it did work, the ripples would merge with each other rather than be independent and overlap.

    It is something I can try again when I have tweaked the lifespan of the ripple and its overall effect and see which version I prefer.

    Thanks for the code example!
    Off to work, will update when I have made some progress :)
     
  13. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,348
    Using a Texture2D and point sampling / loading individual pixel values should produce identical results as sampling a single layer of a 1x1 pixel texture array. If you were getting different results it would be due to implementation differences rather than functionality.