Search Unity

Am I over complicating this random function?

Discussion in 'Shaders' started by TommyPoulin, Feb 5, 2017.

  1. TommyPoulin

    TommyPoulin

    Joined:
    Feb 5, 2017
    Posts:
    14
    Code (csharp):
    1.  
    2. inline float Rand1(float3 seed)
    3. {
    4.    float combinedSeed = dot(seed, NonAlignmentMuls.xyz);
    5.    float remainder = fmod(combinedSeed, PI);
    6.    float rand = frac(sin(remainder)*BigNonAlignmentMul);
    7.  
    8.    combinedSeed = dot(seed, rand.xxx);
    9.    remainder = fmod(combinedSeed, PI);
    10.    rand = frac(sin(remainder)*BigNonAlignmentMul);
    11.  
    12.    newSeed.xyz = rand.xxx;
    13.  
    14.    return rand;
    15. }
    16.  
    Okay so here's the function I've got so far. It's just based on "that canonical one-liner I found on the Internet."

    Code (csharp):
    1.  
    2. float rand(vec2 co)
    3. {
    4. return fract(sin(dot(co.xy ,vec2(12.9898,78.233)))*43758.5453);
    5. }
    6.  
    The so-called NonAlignmentMuls and BigNonAlignmentMul are just constants I've defined (though wasn't entirely sure what to call them).

    BigNonAlignmentMul = 43758.5453

    NonAlignmentMuls = float4(12.9898, 46.4321, 78.233, 138.8765)

    The purpose of the fmod in there is just to make sure it functions correctly on certain GPUs (in phones) that take shortcuts with their sin functions. I saw someone recommend doing it this way when I was researching how the original function worked. I admit it's probably overkill in and of itself.

    newSeed is just a static global I'm writing to so I can use it as the seed if I need to generate another random number.

    The real thing I'm really uncertain of is the fact that I'm basically generating one random number to generate a 2nd random number, but... it gives good results.

    When I don't include those extra lines of code (8-10) I get a lot of artifacts showing up at certain angles (especially when viewing an object close-up, more prominently on objects that have a lot of curvature). I think I'm basically "doubling up" on the randomness, but I can't help but feel there is probably a much simpler way of achieving the same results. However, I've been tinkering with it but no matter what I try I can't find another way to get the same results.

    Here's a picture of what it looks like without lines 8-10 (in an extreme close up of a sphere): UglyLines.png

    Here is the exact same area of the sphere but with the extra lines included: LinesFixed.png
     
  2. Reanimate_L

    Reanimate_L

    Joined:
    Oct 10, 2009
    Posts:
    2,788
    do you need them to update every frame, or just generated once at the beginning?
     
  3. TommyPoulin

    TommyPoulin

    Joined:
    Feb 5, 2017
    Posts:
    14
    At the moment I'm just writing a general purpose function library for random numbers (all of said functions are currently experiencing the same issue, with the same solution, that I'm having the same doubts about).

    Regardless of how it's going to be used it will be something that needs updating every frame. I fully acknowledge that if it just needed to be generated once the beginning it would be vastly simpler to just set some uniform value in the material to a random number.

    So given that it's going to be something updated every frame.... What do you think, too complicated?
     
  4. Reanimate_L

    Reanimate_L

    Joined:
    Oct 10, 2009
    Posts:
    2,788
    i don't it would be a problem, beside the only way to make sure about it is testing it on your actuall game to see the performance.
    if you still in doubt maybe try to use the noise function from playdead, since it's basically already proven on actuall game
     
  5. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    Well, one thing, that fmod(combinedSeed, PI) is going to produce a different result than not having it. The problem is sin(PI) and sin(0.0) are both == 0.0, but sin(PI*0.5) and sin(PI*1.5) are 1 and -1 respectively, and thus frac(sin(0.1)) and frac(sin(PI+0.1)) will be different values.

    Try using fmod(combinedSeed, TAU) instead (tau = pi * 2.0 if you're unfamiliar with that term).
     
  6. jvo3dc

    jvo3dc

    Joined:
    Oct 11, 2013
    Posts:
    1,520
    Why the sine in there? It's a pretty expensive function, which does not seem to help much in the generation of random numbers. My common approach to "random" in a shader is just:
    Code (csharp):
    1.  
    2. frac(dot(value, seed));
    3.  
    I sometimes combine multiple of these depending on the "randomness" needed.

    A dot product is generally not very expensive. The frac instruction is a little less common for a GPU, but I don't really see a way around it for this purpose.
     
  7. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    The noise function in the original post is one that's been floating around the internet for several years and gets copied over and over. It's been"good enough" for most things most of the time, especially on PC and consoles. On mobile it is mildly expensive, but mobile also is getting way more powerful lately, the bigger problem is on mobile it tends to look worse more often likely due to differences in the sine calculation. The fmod tau might help reduce the differences, but @jvo3dc is right that it is probably not needed. Also, as a note, the in shader noise function used by Playdead is the exact same "canonical one-liner" from the first post. Otherwise they use blue noise texture lookups as blue noise is much more expensive to calculate.

    The noise I've recently started to use recently is a sine-less set found here:

    https://www.shadertoy.com/view/4djSRW
     
    Last edited: Feb 6, 2017
  8. jvo3dc

    jvo3dc

    Joined:
    Oct 11, 2013
    Posts:
    1,520
    True, it all depends on the intended goal. It's actually not random at all. It's a stable noise generated from a set of linear ramps, which is usually exactly what you need in a shader. Perfect for randomizing objects.

    For small more complex noise a small texture is also perfectly fine. Those typical 4x4 screen space masks to randomize ssao or other screen space effects barely have any overhead in texture lookups, because the texture is so small.
     
  9. TommyPoulin

    TommyPoulin

    Joined:
    Feb 5, 2017
    Posts:
    14
    Code (csharp):
    1.  
    2. inline float Rand1(float3 seed)
    3. {
    4.    float combinedSeed = dot(seed, NonAlignmentMuls.xyz);
    5.    float rand = frac(frac(combinedSeed)*BigNonAlignmentMul);
    6.  
    7.    rand = frac(frac(combinedSeed * rand)*BigNonAlignmentMul);
    8.  
    9.    newSeed.xyz = rand.xxx;
    10.  
    11.    return rand;
    12. }
    13.  
    Here's the simplified code I've ended up with. Thank you bgolus and jvo3dc for the helpful suggestions.

    I've taken your advice and completely gotten rid of the sin functions (though thank you anyway bgolus for the tau suggestion; I was definitely mucking up the distribution only using pi).

    jvo3dc your extremely simplistic function didn't quite produce the results I was looking for so I ended up trying to increase the randomness by multiplying by a big number again. It gave some pretty strange results. I ended up with one strip of "snow" getting darker at either edge before turning into solid black. Pretty sure that's what the sin function was trying to prevent.

    I ended up basically just replacing the sin functions with extra frac functions (which I think makes it a bit more like the functions bgolus suggested).

    I also changed the way the final random value is calculated (which coincidentally involved simplifying it a bit more). This way just gets rid of some remaining artifacts I didn't notice until I tried using the floor of the world space as the seed.

    Overall I'm much happier with it.
     
  10. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    The problem with any pseudo random function, like the original sine based one, is the various "magic" numbers are chosen to produce a result that looks random as much of the time as possible, but usually only work well when all the "parts" are in place.

    Also, that's a lot of frac()s.
     
  11. TommyPoulin

    TommyPoulin

    Joined:
    Feb 5, 2017
    Posts:
    14
    Code (csharp):
    1.  
    2. inline float Rand1(float3 seed)
    3. {
    4.    float3 combinedSeed = frac(seed * NonAlignmentMuls.xyz);
    5.    float rand = dot(combinedSeed, dot(combinedSeed, seed.yzx + NonAlignmentMuls.xyz));
    6.    rand = frac(frac(rand)*BigNonAlignmentMul);
    7.  
    8.    newSeed.xyz = rand.xxx;
    9.  
    10.    return rand;
    11. }
    12.  
    Final update

    Okay so after having made the last post I noticed there were still a few artifacts so I kept fiddling with it. This new function has no noticeable artifacts at all (as far as I can tell). Plus I was finally able to stop doing that weird "generating a random number with a random number" thing.

    As justification for not just giving up and using those functions from ShaderToy, it started to get repetitious (when using the floor of the world space as the seed and only at a huge size scale):
    Pattern.png

    My function does not do that:
    NoPattern.png
    So take that, random person from ShaderToy. My function is slightly better than yours... in the most pointless way possible.

    Yes, the fact that I'm still working on this is just obsessive at this point....
     
    bgolus likes this.