Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Draw signal strength scrolling graph with a shader...

Discussion in 'Shaders' started by Innovine, May 22, 2019.

  1. Innovine

    Innovine

    Joined:
    Aug 6, 2017
    Posts:
    522
    Hi, I would like to render a waterfall view of signal strength, looking something like this:


    You can see the animated version here: http://k3fef.com:8901/
    The data just scrolls upwards at a constant rate, with the latest data rendered along the bottom row. Ideally I'd like to pass in an array of floats, and have the shader render this along the bottom, while also handling the scrolling.

    If there's a nicer way to do this than a shader, maybe with SetPixels() and scrolling UVs, please let me know!

    Some tips would be really appreciated :)
     
  2. Sh-Shahrabi

    Sh-Shahrabi

    Joined:
    Sep 28, 2018
    Posts:
    56
    Interesting problem :D As far as I can see here are the things you need to do in order:
    1. You need to first vectorize your signal strength, in a form that aligns with your warp. In a simpler terms, if you want to have a graph which is portrait by a texture which has dimensions of 5200 in 400, which would resemble the graph above, your signal strength would need to be packed in a vector (array) of length 5200, or a division of that (like every two pixel might share the same resolution, 5200/2 or 4 or ...)
    2. Then everytime you read your signal strength, you write the info to a static array on the CPU side, with a fixed size mentioned in step one. You need to now copy this to the GPU on every frame. You need to look up how to do this, when you link a Render texture to a material, you don't need to relink it every time you change the Render texture since it is a reference type, and the material only has a reference to the render texture. Arrays are also reference type in C# so I assume it will be the same. Meaning you link it once to your shader and then just update it individual members of the array and the GPU will get the correct data automatically. Alternatively you can also read the strength signals in GPU and write it to a gpu buffer which the shader will read directly but that's way too much work.
    3. Now it is time to visualize it. Here on the CPU side you would probably need to create a temp renderTexture and a an actual one, since you can't read and write from the same buffer. What you do is, you Blit your actual rendertex in your temp one, then you blit your temp one in your actual one, but with a shader which does the following: for all rows, copy and overwrite the value of its pixels with the pixels one row below it (you practically move the entire render texture one up), except the very first row, where you use your uv index, to read the Array of signal strength you put in from CPU side, and set it as pixel values.

    The problem with the above approach is how to correctly provide a proper vectorised data through Unity's material system. If all your threads are going to be in one GPU blocks, it won't be a problem since the array can just be coppied to the shared memory, but it would still be ideal if you can pass on the data in away, where each thread accesses an index close to it. You might need to use compute shaders for that.

    Long story short, there is no easy way to do this accurately and dynamically with just panning. If you don't need to 'actually' work, just looks like it does, you can easily fake it with panning though
     
  3. mgear

    mgear

    Joined:
    Aug 3, 2010
    Posts:
    9,350
    one idea,
    - use customrendertexture, double buffered so can read from itself
    - draw float array into texture2d of size 5200x1
    - use that texture2d to draw into your customrendertexture bottom pixel row
    - customrendertexture keeps scrolling up by reading 1 pixel below

    tested with a 1024x1 perlin noise texture generated on the bottom:
    scrollingperlin.gif
     
    Sh-Shahrabi and bgolus like this.
  4. Sh-Shahrabi

    Sh-Shahrabi

    Joined:
    Sep 28, 2018
    Posts:
    56
    That's awesome. I haven't used costume rendertexutre much so far, gotta look in to them more. A question, what is the advantage of using a tex2D over an array?
     
  5. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    I was going to recommend something like this approach, though I wasn't going to use a CustomRenderTexture to pan it, rather just pan the texture in the shader by passing in a UV offset, and when copying the texture2D for each line to the larger texture, offset it to where the "current" bottom is. I'd also use CopyTexture() to do that instead of a Blit(), and then you don't even need a render texture, though it doesn't really matter either way. CopyTexture() is nice because you can just copy over the pixels you want to replace.

    https://docs.unity3d.com/ScriptReference/Graphics.CopyTexture.html

    Arrays on the CPU and arrays on the GPU are not the same data and exist in completely separate memory. There's no "link" (reference) happening, they get copied from one to the other. At least 3 if not more copies in fact to get the data from where it's set in C# to the GPU. One when you call SetArray*() to copy the data across the C# to C++ barrier, another to copy that data into a C++ array that's representing a CBUFFER or similar structure to give to the drivers / graphics API, and one more to actually copy that data from the CPU memory to the GPU memory. And that's not counting any internal machinations that might be happening with the data, within Unity's C++ code, the graphics driver, and even the GPU itself.

    Even textures aren't really references. When you assign a texture to a material, you're assigning something akin to a reference to a reference. There's still (at least) two copies of that data, one on the CPU and one on the GPU, it's just been decided that the asset is going to be what points at either the CPU or GPU side copy of the texture. But if you set a pixel on a texture using only SetPixel, nothing will happen to the texture on the GPU. You need to also call Apply(), which ends up many of the same steps as above. The SetPixel is doing the C# to C++ copy, but the Apply is telling the C++ side to send the data to the driver, which then uploads it to the GPU to update the appropriate memory range. It gets a little confusing because the asset is really only the C++ & graphics driver side copy of the texture. If you want to access it in C# you're using SetPixels / GetPixels which is copying the data between C++ and C#. The graphics API is being told "here's a reference to a chunk of memory that is a texture", and the graphics API is saying "okay, here's an id to use when you want to use it elsewhere". When you assign a texture asset to a material property, internally Unity is telling the graphics API "okay, use texture #12 here".

    Render Textures are a complete different beast because they do not exist in any form on the CPU side apart from the data needed to define its size and format, and that "reference to a reference" in that asset.
     
    Last edited: May 22, 2019
    Sh-Shahrabi likes this.
  6. Innovine

    Innovine

    Joined:
    Aug 6, 2017
    Posts:
    522
    Thanks guys :)
    I'll be using this in a game, so it doesn't have to be perfect; but scanning the virtual airwaves for signals is an important mechanic, so it needs to work to a reasonable degree. I have not yet implemented the signals yet, so I can choose whatever. A packed array of floats or ints will be fine and I've seen some examples of how to pass those to a shader. The scrolling is the bit I'm most interested in :)
     
  7. mgear

    mgear

    Joined:
    Aug 3, 2010
    Posts:
    9,350
    Innovine likes this.
  8. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    Passing in an array of floats is no more efficient than passing in a texture of the same data. The texture can even be more efficient since you can pass the data in as an R8 so you're only passing a single byte of data per pixel rather than a float array which will always be copied around as a full 32 bit float per "pixel", so 1/4th the data to copy and access. Plus since you want to end up using it as a texture anyway, it's good to have it in that format so you can take advantage of the texture sampling hardware.

    So, basic steps:

    Make a simple unlit shader that takes two textures, a color gradient texture and the signal intensity, and an offset float. In the shader take the vertex UVs and offset them in the direction you want to scroll by the offset property. Then sample the signal texture using those UVs, and sample the color gradient texture using the signal intensity.

    Create a RenderTexture using the format R8 that's got enough width and height for the amount of precision you want to show to the player. This can be created and assigned to your material from script, or you can make a render texture asset, it doesn't really matter, assuming you don't want / need to change the resolution dynamically.

    Make a script that references that render texture and the material and either takes the signal data, or is itself what generates that data, and has a row index int value. On start create a Texture2D from script that's the same format & width as the render texture and only 1 pixel high. On Update use SetPixels() and Apply() to update the Texture2D, as well as increment a row index value. Wrap the row index using index = index % rendertextureheight. Then use CopyTexture() to copy the data from the Texture2D to a single row of the RenderTexture using the row index. Update the material's offset value to be (float)rowindex / (float)rendertextureheight).

    Vioala!
     
    Sh-Shahrabi likes this.
  9. Innovine

    Innovine

    Joined:
    Aug 6, 2017
    Posts:
    522
    Doesn't sound too bad.. can't wait to try this, thanks!!
     
  10. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    One other note. The Texture2D should not have mip maps. The RenderTexture can if you want, but you'll need to call GenerateMips() on it after the CopyTexture() to update them.