Search Unity

  1. Calling all beginners! Join the FPS Beginners Mods Challenge until December 13.
    Dismiss Notice
  2. It's Cyber Week at the Asset Store!
    Dismiss Notice

2D Water Flow Shader

Discussion in 'Shaders' started by UnetDev, Nov 28, 2019.

  1. UnetDev

    UnetDev

    Joined:
    Aug 28, 2017
    Posts:
    49
    Hey. I came across this post: https://gamedev.stackexchange.com/q...-render-2d-top-down-tiled-directed-water-flow

    And the shader was presented in the answer. I tried to reproduce it, but did not get the effect that its author received.


    My try:
    Code (CSharp):
    1. Shader "2DWater"
    2. {
    3.     Properties
    4.     {
    5.         _MainTexture("MainTexture", 2D) = "white" {}
    6.         _Flow ("Flow", 2D) = "white" {}
    7.         _Wave ("Wave", 2D) = "white" {}
    8.     }
    9.     SubShader
    10.     {
    11.         Tags { "RenderType"="Opaque" }
    12.         LOD 100
    13.  
    14.         Pass
    15.         {
    16.             CGPROGRAM
    17.             #pragma vertex vert
    18.             #pragma fragment Frag
    19.  
    20.             #include "UnityCG.cginc"
    21.  
    22.             // Colour texture / atlas for my tileset.
    23.             sampler2D _MainTexture;
    24.             // Flowmap texture.
    25.             sampler2D _Flow;
    26.             // Wave surface texture.
    27.             sampler2D _Wave;
    28.  
    29.             // Tiling of the wave pattern texture.
    30.             float _WaveDensity = 0.5;
    31.             // Scrolling speed for the wave flow.
    32.             float _WaveSpeed  = 5.0;
    33.  
    34.             // Scaling from my world size of 8x8 tiles
    35.             // to the 0...1
    36.             float2 inverseFlowmapSize = (float2)(1.0f/8.0f);
    37.          
    38.             struct v2f
    39.             {
    40.     // Projected position of tile vertex.
    41.     float4 vertex   : SV_POSITION;
    42.     // Tint colour (not used in this effect, but handy to have.
    43.     fixed4 color    : COLOR;
    44.     // UV coordinates of the tile in the tile atlas.
    45.     float2 texcoord : TEXCOORD0;
    46.     // Worldspace coordinates, used to look up into the flow map.
    47.     float2 flowPos  : TEXCOORD1;
    48.     };
    49.  
    50.         v2f vert(appdata_full IN)
    51. {
    52.     v2f OUT;
    53.  
    54.     // Save xy world position into flow UV channel.
    55.     OUT.flowPos = mul(unity_ObjectToWorld, IN.vertex).xy;
    56.  
    57.     // Conventional projection & pass-throughs...
    58.     OUT.vertex = UnityObjectToClipPos(IN.vertex);
    59.     OUT.texcoord = IN.texcoord;
    60.     OUT.color = IN.color;
    61.  
    62.     return OUT;
    63. }
    64.  
    65. float2 WaveAmount(float2 uv, float2 sampleSite) {
    66.     // Sample from the flow map texture without any mipmapping/filtering.
    67.     // Convert to a vector in the -1...1 range.
    68.     float2 flowVector = tex2Dgrad(_Flow, sampleSite * inverseFlowmapSize, 0, 0).xy
    69.                         * 2.0f - 1.0f;
    70.     // Optionally, you can skip this step, and actually encode
    71.     // a flow speed into the flow map texture too.
    72.     // I just enforce a 1.0 length for consistency without getting fussy.
    73.     flowVector = normalize(flowVector);
    74.  
    75.     // I displace the UVs a little for each sample, so that adjacent
    76.     // tiles flowing the same direction don't repeat exactly.
    77.     float2 waveUV = uv * _WaveDensity + sin((3.3f * sampleSite.xy + sampleSite.yx) * 1.0f);
    78.  
    79.     // Subtract the flow direction scaled by time
    80.     // to make the wave pattern scroll this way.
    81.     waveUV -= flowVector * _Time * _WaveSpeed;
    82.  
    83.     // I use tex2DGrad here to avoid mipping down
    84.     // undesireably near tile boundaries.
    85.     float wave = tex2Dgrad(_Wave, waveUV,
    86.                            ddx(uv) * _WaveDensity, ddy(uv) * _WaveDensity);
    87.  
    88.     // Calculate the squared distance of this flowmap pixel center
    89.     // from our drawn position, and use it to fade the flow
    90.     // influence smoothly toward 0 as we get further away.
    91.     float2 offset = uv - sampleSite;
    92.     float fade = 1.0 - saturate(dot(offset, offset));
    93.  
    94.     return float2(wave * fade, fade);
    95. }
    96.  
    97. fixed4 Frag(v2f IN) : COLOR
    98. {
    99.     // Sample the tilemap texture.
    100.     fixed4 c = tex2D(_MainTexture, IN.texcoord);
    101.  
    102.     // In my case, I just select the water areas based on
    103.     // how blue they are. A more robust method would be
    104.     // to encode this into an alpha mask or similar.
    105.     float waveBlend = saturate(3.0f * (c.b - 0.4f));
    106.  
    107.     // Skip the water effect if we're not in water.
    108.     if(waveBlend == 0.0f)
    109.         return c * IN.color;
    110.  
    111.     float2 flowUV = IN.flowPos;
    112.     // Clamp to the bottom-left flowmap pixel
    113.     // that influences this location.
    114.     float2 bottomLeft = floor(flowUV);
    115.  
    116.     // Sum up the wave contributions from the four
    117.     // closest flow map pixels.  
    118.     float2 wave = WaveAmount(flowUV, bottomLeft);
    119.     wave += WaveAmount(flowUV, bottomLeft + float2(1, 0));
    120.     wave += WaveAmount(flowUV, bottomLeft + float2(1, 1));
    121.     wave += WaveAmount(flowUV, bottomLeft + float2(0, 1));
    122.  
    123.     // We store total influence in the y channel,
    124.     // so we can divide it out for a weighted average.
    125.     wave.x /= wave.y;
    126.  
    127.     // Here I tint the "low" parts a darker blue.
    128.     c = lerp(c, c*c + float4(0, 0, 0.05, 0), waveBlend * 0.5f * saturate(1.2f - 4.0f * wave.x));
    129.  
    130.     // Then brighten the peaks.
    131.     c += waveBlend * saturate((wave.x - 0.4f) * 20.0f) * 0.1f;
    132.  
    133.     // And finally return the tinted colour.
    134.     return c * IN.color;
    135. }
    136.             ENDCG
    137.         }
    138.     }
    139. }
    upload_2019-11-28_13-13-8.png
    Could you tell me what my mistake is?
     
    yuliyF likes this.
  2. UnetDev

    UnetDev

    Joined:
    Aug 28, 2017
    Posts:
    49
  3. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    310
    It would help if you would describe what IS working and what is NOT working.

    I see an animated image for the effect you want, but your example you're using different input images, and your image isn't animated. So ... without mindreading, I ahve no idea what you're saying is wrong?
     
  4. UnetDev

    UnetDev

    Joined:
    Aug 28, 2017
    Posts:
    49
    This shader is not mine. As far as I understand, his task is to make the static image of water to move, but for some reason this does not work as it should. I also understand that the direction of the water flow is set using the Flow texture of 8x8 pixels (field float2 inverseFlowmapSize = (float2) (1.0f / 8.0f); also points to this),
    but the movement still doesn’t happen.
     
  5. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    310
    No movement at all?

    Are you testing in game or in editor? By default, the editor won't animate shaders
     
  6. UnetDev

    UnetDev

    Joined:
    Aug 28, 2017
    Posts:
    49
    Yes, I run the scene for the test.
     
  7. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    310
    Try deleting lines 108 and 109.

    But, really ... until you start describing exactly what's happening vs what you expect, there's not much to help with.
     
  8. UnetDev

    UnetDev

    Joined:
    Aug 28, 2017
    Posts:
    49
    If I could describe what is happening in it line by line, I think I would not ask the forum for help))
     
  9. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    310
    I'm not asking you to explain the entire shader, I'm asking you to describe what you expected to see on screen vs what you actually see. This is the first step in fixing a problem - until you do that, you haven't started.

    Also, debugging simple shaders like this one is very easy, you just need to do small edits and see what happens. Unlike most shaders, this is very simple code, 100% of it can be learned from scratch in a couple of hours or less. I would strongly recommend you do some basic tutorials in scripting, so that you feel confident editing the shader line by line to see what happens. This will save you time right now, and save you lots of time in future.

    (You don't need to understand every line of code, but you need to know the basics of how lines of code are executed one after the other, and roughly what they do.)
     
  10. mgear

    mgear

    Joined:
    Aug 3, 2010
    Posts:
    5,598
    well the issue is that it doesn't do anything, water doesn't scroll.

    it has _Time there so something should animate, but it also uses worldspace XY coordinates,
    so could be problem with the mesh location, scale of your mesh, or how the UV maps should be in the mesh?
     
  11. UnetDev

    UnetDev

    Joined:
    Aug 28, 2017
    Posts:
    49
    I am at the stage of studying shaders. I read the Cg tutorial and rummaged through the documentation for ShaderLab. But I still don’t understand some points. For example:

    wave += WaveAmount(flowUV, bottomLeft + float2(1, 0));
    wouldn't bottomLeft always be 0.0? That is, the lower left corner of the square?
    Why is the addition operation bottomLeft + float2 (1, 0) performed in the parameters, if you could just specify float2 (1, 0) and have the same effect?


    float2 flowVector = tex2Dgrad(_Flow, sampleSite * inverseFlowmapSize, 0, 0).xy
    * 2.0f - 1.0f;
    this thing I can’t understand at all
     
  12. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    310
    bottomLeft is calculated here:

    Code (CSharp):
    1. // Clamp to the bottom-left flowmap pixel
    2.     // that influences this location.
    3.     float2 bottomLeft = floor(flowUV);
    UV's don't have to go 0...1, they can go 2 ... 3 ... 4 ... etc. So, one common approach in shaders is to UV-map across an area with - say - UV 0...10, and then you can use floor(UV) to get a repeating 0..1, 0..1, 0..1 ... but also get the full 0...1, ...2, ...3, ...4 at the same time (for different parts of your shader).
     
  13. UnetDev

    UnetDev

    Joined:
    Aug 28, 2017
    Posts:
    49
    as far as I understand, the movement should occur on the _Wave texture (the last in the inspector window on the screenshot) using tex2Dgrad(_Wave, waveUV, ddx (uv) * _WaveDensity, ddy (uv) * _WaveDensity);
    where waveUV coordinates that are obtained using flowVector * _Time * _WaveSpeed;
    but the problem is that I don’t quite understand the line
    float2 flowVector = tex2Dgrad (_Flow, sampleSite * inverseFlowmapSize, 0, 0) .xy
    * 2.0f - 1.0f;
    to imagine further calculations
     
  14. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    310
    My approach is usually to delete big parts of the shader, make a simpler version, and build it up in stages. That way, eventually you find the part that isn't doing what you expected it to do, and you can fix it.

    e.g. on line 122, you could immediately return the value of wave - that will show you if the function that calculates "wave" is even working at all.

    Something like:

    Code (CSharp):
    1. float4 result = float4(0,0,0,1); // black, fully opaque
    2. result.rg = wave; // assign the wave to the R and G channels in result
    3. return result;
     
  15. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    310
    ...Or at Line 115, do the same with flowUV, and then with bottomLeft: try returning each of them, see if they look like they have the values you'd expect if they were correct.
     
  16. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    310
    As per the comment on the line above, the author is using it to disable mipmapping (vs using a normal tex2D).

    I'm guessing: so that the shader samples the blocky, sharp-edged flowmap, rather than automatically blurring it by using mipmaps.

    It would be worth returning this value immediately (see above), and then compare that to returning:

    tex2D( _Flow, sampleSite * inverseFlowmapSize)

    ...you should be able to clearly see the difference/purpose that way
     
  17. UnetDev

    UnetDev

    Joined:
    Aug 28, 2017
    Posts:
    49
    I read theoretical material about mipmap, and that tex2D can take a lower quality texture, since the object can be far from the camera.

    sampleSite * inverseFlowmapSize
    I don’t understand this calculation, since it seems to me that every time
    WaveAmount(flowUV, bottomLeft);
    will have bottomLeft as 0,0
     
  18. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    310
  19. UnetDev

    UnetDev

    Joined:
    Aug 28, 2017
    Posts:
    49
    I tried to do so, and was very surprised because tex2D(_Flow, bottomLeft + float2 (1.0)) returns
    upload_2019-12-2_1-59-35.png

    but I figured it would be only
    upload_2019-12-2_1-59-43.png
     
    Last edited: Dec 2, 2019 at 12:05 AM
  20. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    310
    Try just returning bottomLeft itself - so you can see what actual values it contains (if you return tex2D( ..., bottomLeft), then you're obscuring the value by mixing it with your texture, so you still don't know what value it has).
     
  21. UnetDev

    UnetDev

    Joined:
    Aug 28, 2017
    Posts:
    49
    Thank you for the method by which I can now output values to an object, and then just use the eyedropper to see what happens on each pixel, this is very cool!

    So, I understand more now what flowUV is. The values that take real world coordinates inside, and if you move the object, then a palette from black to red (along the x axis) and from black to green (along the y axis) will be visible, provided that the swizzle will be blackColor.rg = flowUV.xy;

    bollomLeft there will always be 0.0 in WaveAmount(flowUV, bottomLeft); since the nearest integer value for all values from 0 to 1 will be 0.
    Therefore, the expression tex2Dgrad(_Flow, bottomLeft * inverseFlowmapSize, 0, 0).xy will take the lower left pixel in the _Flow texture, whose values are r == ~0.77, and g == ~0.95 (I indicate approximately, since I do not remember the exact values). there are also additional actions * 2.0f - 1.0f; which turn ~ 0.77 and ~ 0.95 into ~ 0.55 and ~ 0.90 respectively
    and the result is always the same for all situations:
    Code (CSharp):
    1.     WaveAmount(flowUV,  bottomLeft);
    2.     or
    3.     WaveAmount(flowUV, bottomLeft + float2(1, 0));
    4.     or
    5.     WaveAmount(flowUV, bottomLeft + float2(1, 1));
    6.     or
    7.     WaveAmount(flowUV, bottomLeft + float2(0, 1));
    always returns the same values ~ 0.55 and ~ 0.90


    I understand the normalization stage, I see no reason to write something about it

    But then there is some kind of calculation
    float2 waveUV = uv * _WaveDensity + sin((3.3f * sampleSite.xy + sampleSite.yx) * 1.0f);
    that I don’t understand and which displays this:
    upload_2019-12-2_8-20-46.png
    then I did all 3 offsets that were:
    Code (CSharp):
    1.  
    2.     WaveAmount(flowUV, bottomLeft + float2(1, 0));
    3.     or
    4.     WaveAmount(flowUV, bottomLeft + float2(1, 1));
    5.     or
    6.     WaveAmount(flowUV, bottomLeft + float2(0, 1));
    and in all these situations there was a shift in the corresponding directions (for example, with + float2(1, 1), the lower left black square turned yellow)


    and here is the line that caused me a lot of questions:
    waveUV -= flowVector * _Time * _WaveSpeed;
    as far as I understand, it should make the colors in the grid above change with time, but this does not happen
    BUT, if I delete * _WaveSpeed part, then the grid starts to gradually change its colors in some places to black

    and at that moment my brains began to boil and I ceased to understand why this was happening ...
     
  22. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    310
    Looking purely at this line ... we can see it is taking the UV, and subtracting a value from it (which means it subtracts from U and V simultaneously).

    We know - by definition - that has the effect that anything using that UV will seem to "flow" up and to the right (because it's sampling a bit lower and to the left on each timestep ... more and more over time).

    NOW ... if that is "gradually chang[ing] ... to black", that means it's eventually sampling a UV location that returns 0, i.e. the color black.

    ...which suggests that it's sampling off the edge of the texture, and off the edge of the texture, it's returning 0.

    SO .... check your texture import settings! What clamp mode are you using? Maybe you need to set your texture to clamp-mode = repeat?
     
  23. UnetDev

    UnetDev

    Joined:
    Aug 28, 2017
    Posts:
    49
    Yes you are right! I thought that the default texture in the "repeat" mode, but it is not.
    This is what happened when I set the _Wave texture to "repeat" mode:


    And two questions remain:
    1) Why does it work without "_WaveSpeed"?
    2) Why is the texture not moving?
     
    a436t4ataf likes this.
  24. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    310
    Reading the code, _WaveSpeed appears to be a fixed value: the number 5.

    i.e. it merely increases/decreases the overall speed. If you remove it, that's the same as having a _WaveSpeed of 1 (because you were multiplying by _WaveSpeed).

    I don't know what else is wrong now, but ... keep debugging :)
     
  25. UnetDev

    UnetDev

    Joined:
    Aug 28, 2017
    Posts:
    49
    I also thought that this is just an acceleration of movement, but this is not so, since it simply does not work with this value.
    On the example of the gif, which I threw off at the beginning, it is clear that the texture is moving, but I don’t understand which part of the code moves this texture, it doesn’t move for me.