Search Unity

Resolved Jitter artefacts when using randomness to get a texture channel

Discussion in 'Shaders' started by CarpetThomas, Jan 26, 2021.

  1. CarpetThomas

    CarpetThomas

    Joined:
    Jan 26, 2021
    Posts:
    9
    I'm making grass, for which I wanted 3 different sprites to go randomly across a field of quads.
    They are respectively in the red, green and blue channel of the main texture, like so :


    In the shader, when manually entering 0,1 or 2 as an index, it works great.

    (I tried with 0,1 and 2, I just can't link more images)

    I then use a hash fonction to get a random value from 0 to 1, based on the localPos of each quad ( the localPos stores the position in world space of each quad's centered pivot ).
    I multiply the random by 2.99 to get a value from 0 to 2.99 and then cast to int to get 0, 1 or 2.

    This is the result I'm getting :


    As you can see, there's a jittery effect happening on some quads, as if they were switching textures per pixel or something.

    Is there something I'm missing?

    Thanks a lot in advance,
     

    Attached Files:

  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,339
    The issue you're having isn't so much with the code you're showing, but the nature of interpolated data and floating point math. It's not even really a bug.

    You're calculating some local position value in the vertex shader and then passing it to the fragment shader. That value is presumably identical on every vertex, so it's not misguided to expect the interpolated value the fragment receives would also be the same ... but you'd be wrong.

    Because the values are being interpolated, and floating point math isn't perfectly precise, the value the fragment gets in the end can be off by a tiny bit +/- the value the vertices all calculated.

    There are four solutions I can think of:

    1. Quantize the data you're using, both in the vertex shader and again in the fragment shader. Something like:
    Code (csharp):
    1. // vertex
    2. o.localPos = // local position
    3. // quantize to 1/1000 accuracy, with a 0.5 / 1000 offset
    4. o.localPos = (floor(o.localPos * 1000.0) + 0.5);
    5.  
    6. // fragment
    7. float3 localPos = floor(i.localPos); // optionally / 1000.0 here after the floor
    Pros: Works on any hardware, lets you keep your calculations in the vertex shader. Cons: Depending on the density you need, and thus the amount you have to multiply the value by, it may cause problems as you get further away from the world origin, as you'll run in to floating point precision problems faster than you would normally. Eventually the floating point precision will be worse than the quantization is correcting for.


    2. Use interpolation modifiers to disable interpolation. Adding
    nointerpolation
    in front of an interpolated value will tell the GPU to skip any interpolation and use the triangle's invoking vertex data unmodified.
    Code (csharp):
    1. struct v2f {
    2.   // other stuff
    3.   nointerpolation float3 localPos : TEXCOORD2;
    4. };
    Pros: Again, keeps your calculations in the vertex shader. Cons: Not supported on all mobile hardware. Though that's probably a non-issue for this.


    3. Calculate the value in the fragment shader. If your value is something that can be calculated from data you have access to in the fragment shader, you should! I know I listed "keep your calculations in the vertex shader" as a "pro" in the above examples, and in some cases that can still be a good thing. But if you're using instancing to draw these and the local position is the pivot extracted from the
    unity_ObjectToWorld
    matrix, you're far better off passing the instance ID to the fragment shader and then getting the pivot position from the matrix in the fragment shader directly. Interpolation values has a memory bandwidth cost, and GPUs have gotten faster at doing math than their memory bandwidth has increased.
    Pros: Probably a lot cheaper! Cons: Assumes you're using instancing, which you may not be.


    4. Calculate the "index" in the vertex shader and pass that instead of the local position. You'll want to use the same kind of "+ 0.5" offset to get around the interpolation problem, but because the value is between 0.5 and 3.5 you won't run into the same floating point limitations.
    Code (csharp):
    1. struct v2f {
    2.   // stuff
    3.   float index : TEXCOORD2; // or pack it into the w of a float3 or z of a float2 in the struct
    4. };
    5.  
    6. // vertex
    7. float3 localPos = // local position
    8. o.index = floor(hash(localPos.xz) * 2.99) + 0.5;
    9.  
    10. // fragment
    11. float mask = baseTex[(int)(i.index)];
    Pros: Works for geometry shaders where the fragment might not have enough info to calculate the data in the fragment. Cons: None (apart from you're using a geometry shader).
     
    hopeful likes this.
  3. CarpetThomas

    CarpetThomas

    Joined:
    Jan 26, 2021
    Posts:
    9
    Thanks a lot for the detailed answer ! the nointerpolation worked though I used the index solution to keep the hash in the vertex shader, and used an int to avoid interpolation. I am woondering if keeping it as a float was better in some way ?

    I was also getting the local position of each quad stored in the vertex color, since I'm baking the mesh after instanciating all quads, so getting the value in the fragment wouldn't have been hard, though I'm guessing the interpolation problem would have happened too when passing vertex colors info ..

    Really learned something today thank you !
     
  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,339
    Using an integer means the
    nointerpolation
    modifier is implicit, and is being added by the compiler.

    The draw back is on some GPUs interpolated data is always passed as a
    float4
    worth of data at a time. That means your
    nointerpolation int index : TEXCOORD2;
    is costing the same as if it was an
    int4
    or
    float4
    . Remember, data passed from the vertex to the fragment has a cost. Keeping it a
    float
    and not using
    nointerpolation
    means it can be packed into an existing semantic that’s not already a full
    float4
    reducing the overall potential memory bandwidth usage.

    Some GPUs / APIs will do this packing for you, and may even do it even with mismatched interpolation formats or modifiers. And knowing which will or won’t isn’t something I can tell you ... because I don’t know for sure. And it might not even be a “new vs old” or “mobile vs desktop” gpu thing. For example I’m pretty sure modern Nvidia desktop GPUs pack mismatched data for you, but it doesn’t look like AMD desktop GPUs do? And RDNA (Radeon RX 5000&6000) may even be even worse at it than GCN (basically all AMD GPUs prior to the most recent ones going back over a decade). Or I could be completely wrong, it’s really hard to tell exactly what’s going on in the hardware for sure.
     
  5. CarpetThomas

    CarpetThomas

    Joined:
    Jan 26, 2021
    Posts:
    9
    Oh I see, float packing it is then ! Thanks again !