I am trying to recreate this awesome solution to water flowing
(the well thought out answer below)

I have a function to create the flow map in gdscript (which is translated to just pixel by pixel colors). But I don't know how I should send this data to a shader.

I assume I try and package it all up as one image and set one of the shader's sampler2D uniforms to that image? Or maybe I set each pixel at each pixel coordinate individually?

  • Ok. So here's the whole thing, sans the foam. Blending looks best if the final output is used as a normal map.
    Flow map and animated procedural gradient tiles:

    Displacement noise added to gradients and 4-tile weighted blending. Noise texture is Godot's procedural simplex noise.

    Normal map calculated from gradient derivatives using shader's dFdx and dFdy functions:

    Final shader:

    This is just to demonstrate the whole approach. It can obviously be tweaked and improved.
    Here's the shader code:

    shader_type spatial;
    render_mode async_visible,blend_mix,depth_draw_opaque,cull_back,diffuse_burley,specular_schlick_ggx;
    uniform sampler2D texture_flow : hint_albedo;
    uniform sampler2D texture_noise : hint_albedo;
    uniform float wave_count : hint_range(1.0, 10.0, .5) = 3.0;
    uniform float noise_strength : hint_range(0.0, 1.0, .05) = .4;
    uniform float blend_zone : hint_range(.5, 1.0, .05) = 1.0;
    
    // flow texture sampling function
    vec2 sample_flow(ivec2 tile){
    	return texelFetch(texture_flow, clamp(tile, ivec2(0,0), textureSize(texture_flow, 0)), 0).rg;
    }
    
    
    // generate gradient pixel at given uv for given flow
    // gradient used here is smoothstepped. We can use linear or sine gradients as well.
    float wave_amplitude(vec2 flow, vec2 uv, float count){
    	float vel = length(flow)* 3.0 + 1.0; // velocity
    	vec2 dir = normalize(flow); 
    	uv = mat2(vec2(dir.y, -dir.x), dir) * uv; // direction - rotate via 2x2 matrix
    	float grad = fract( TIME * vel + uv.x * count); // translate
    	return smoothstep(0.0, .5, grad) * smoothstep(1.0, .5, grad); // smoothstep the gradient
    }
    
    
    void fragment() {
    	ROUGHNESS = 0.4;
    	METALLIC = .5;
    	SPECULAR = .5;
    	
    	vec2 uvtex = UV * vec2(textureSize(texture_flow, 0)); // texture coords in flowmap space
    	ivec2 tile = ivec2(uvtex); // tile index
    	vec2 tile_fract = fract(uvtex); // tile fraction
    
    	uvtex += texture(texture_noise, UV).rg * noise_strength; // uv noise
    	
    	// sample 4 nearest tiles and do weighted blending
    	vec2 baseTile = floor(uvtex - vec2(.5)) + vec2(.5);
    	float a = 0.0;
    	float w_total = 0.0;
    	for(int j = 0; j<=1; ++j){
    		for(int i = 0; i<=1; ++i){
    			float d = length(uvtex - (baseTile + vec2(float(i), float(j)))); 
    			float w = blend_zone - clamp(d*d, 0.0, blend_zone);
    			a += wave_amplitude(sample_flow(ivec2(floor(baseTile)) + ivec2(i, j)), uvtex, wave_count) * w;
    			w_total += w;
    		}	
    	}
    	a /= w_total;
    
    	// calculate normal
    	vec3 nmap = vec3(dFdx(a), dFdy(a), .3);
    	nmap = normalize(nmap);
    	NORMALMAP = nmap;
    	
    	// some albedo
    	ALBEDO = vec3(0.0, 0.2, 1.0);
    }

XYZ's post helped me get my flow map data to the GPU. When displayed it's blurred (probably shouldn't be an issue right?) Is there a way to ... not blur it? Maybe by flooring the UVs or something?
I also noticed the image is not stretched along the y axis enough but stretching it manually of course seems wrong.
CPU solved flow velocities (right, dots). That data is transferred to the GPU (left). At the bottom of the left GPU part you can see a missing chunk. The image itself however I feel correctly reflects the CPU data shown on the right. Any tips would be much appreciated.

Image data is just image data. It is sent to the GPU essentially uncomressed (there are GPU compression formats but they aare more or less lossless in terms of what the shader gets). You pass them in as a uniform and they can be accessed in shader code with aa sampler2d. The way you sample makes a difference. Using texture or textureLod will use the sampling type set in the editor (linear by default which is blurry). You want to use texelFetch, which samples integer coordinates in the space of the image resolution (pixel coordinates rather than texture uv coordinates). Not sure why the left is the wrong aspect raatio. It may actually be correct in memory and just displayed wrong. O you may have some incorrect uv math that would be pixeled by using pixels as mentioned above. I would start there and report back.

12 days later

lovely!

To see the shader in action check here.
Building walls changes the flow of the water, water always moves toward the drain, and water speeds up if one or more faucets must go through a narrow channel to reach the drain. TexelFetch is not available in GLES2, but it turned out that leaving the project in GLES3 and exporting to HTML5 was not an issue as the shader works just fine in the browser. Hurray!
On close inspection one can see seams between water tiles where water is flowing in different directions but I'm not sure (even with a blend) that this is entirely avoidable because I am translating the UV different amounts and in different directions in different tile squares.

I would love some advice on how you might go about hitting the sides of the terrain tiles with waves. I'm not sure if I should further complicate the water shader sitting beneath the land tiles OR if I should build another shader to apply directly to the land tiles.

How about this. Use simple procedural ramp texture instead of a bitmap. In addition to translation velocity/direction, drive uv rotation per tile too, and also deform uv using additional simplex noise texture. You'll now have more control over how the whole thing looks and moves and you can make tiling less obvious without any tedious cross-tile blending:

just a hint of what to do in the fragment shader:

uniform sampler2D flow_texture: hint_albedo; // velocity and direction driver texture
uniform sampler2D noise_texture: hint_albedo; 

void fragment() {
	// sample direction and velocity from texture
	vec4 flow_sample = texture(flow_texture, UV);
	float direction = flow_sample.r * 6.0;
	float velocity = flow_sample.g * 5.0;
	
	// transform uv 
	mat2 m; // uv rotation matrix
	m[0] = vec2(cos(direction), -sin(direction));
	m[1] = vec2(sin(direction), cos(direction));
	vec2 new_uv = m * UV; // rotate uv
	new_uv.x += TIME*velocity + new_uv.x*16.0;  // translate and scale uv
	new_uv.x += (texture(noise_texture, UV).r )*1.05; // noise uv
	
	float f = fract(new_uv.x);
	f = step(.75, f);
	ALBEDO = mix(vec3(0.0, 0.5, .5), vec3(1.0, 1.0, 1.0), f*.15);
}

You can use the same calculation for border foaming effect, either in separate shader on border tiles, or add it again in the same shader, just differently stepped/masked:

    You can see my change to the shader here. IMHO it looks a great deal better than here.

    I'm not so smart that it was on purpose, but the dark lines on the tiles under the water prevent the player from noticing the white foam from not matching up from tile to tile.

    I'd also like to provide my code for anyone to play around with:

    Click to reveal Click to hide
    shader_type canvas_item;
    
    uniform sampler2D whites; // a image of just the water foam whites
    uniform sampler2D waterTile; // water tile texture
    
    
    uniform sampler2D vectors; // vectors showing water flow direction for each tile
    uniform vec2 tileSize = vec2(62.0,62.0); 
    
    
    uniform vec2 tileMapSize = vec2(26.0, 20.0); // number of grid tiles
    
    uniform float opacity : hint_range(0, 1);
    
    uniform float waveAmplitude = 0.2;
    
    uniform float waveFrequency = 2.0;
    
    uniform float blendingAmount = 0.6;
    
    vec4 getAverageColor(vec2 uv) {
    	// Compute the offsets for sampling neighboring pixels
    	vec2 dx = vec2(1.0 / tileSize.x, 0.0);
    	vec2 dy = vec2(0.0, 1.0 / tileSize.y);
    	
    	// Sample the neighboring pixels and average their colors
    	vec4 sum = texture(waterTile, uv);
    	sum += texture(waterTile, uv + dx);
    	sum += texture(waterTile, uv - dx);
    	sum += texture(waterTile, uv + dy);
    	sum += texture(waterTile, uv - dy);
    	return sum / 5.0;
    }
    
    
    vec2 wave(vec2 uv, float time) {
        return vec2(
            uv.x + sin(uv.y * waveFrequency + time) * waveAmplitude,
            uv.y + sin(uv.x * waveFrequency + time) * waveAmplitude
        );
    }
    
    vec2 grid(vec2 uv, float columns, float rows){
        return fract(vec2(uv.x * columns, uv.y * rows));
    }
    
    void fragment(){
    	ivec2 cd = ivec2(floor(UV * tileMapSize)); 
    	
    	vec4 flowColor = texelFetch(vectors, cd, 0);
    //	float dist = flowColor.w; // I had also previously passed in water tile to land distance
    
    	// convert color from velocity to actual vec2
    	vec2 velocity = (flowColor.xy * 2.0 - vec2(1.0)) * flowColor.z * 15.0;
    	
    	// convert image UV to grid cell UV
    	vec2 gridUV = grid(UV, tileMapSize.x, tileMapSize.y);
    	
    	// apply wave effect to UV
    	gridUV = wave(gridUV, TIME);
    	
    	// get additional foam contribution
    	vec4 foam = texture(whites, gridUV - velocity * TIME);
    	gridUV += -1.0 * velocity * TIME;
    
    	vec4 c = getAverageColor(gridUV) * (1.0 - blendingAmount);
    	if (foam.w > 0.5){
    		c.xyz += foam.xyz * flowColor.z; // foam color is a product of velocity length (passed in also from cpu)
    	}
    	c += texture(waterTile, gridUV) * blendingAmount;
    	c.a = opacity;
    	
    	COLOR = c;
    }

    xyz When you start my game press the bottom-left blue button once. Is that what you mean by a procedural ramp texture? Or is that the bitmap? If I understand you correctly, I can also rotate the UVs which could alleviate the UV-mismatch between water tiles. I'd like to learn how to do that so I'll be studying your code tonight I'm sure for more than a few hours XD.

    For the border effect you presented, that seems like a really smart way to do it, but I feel like I'll need to send in a another texture to the shader to know where the land tiles are... and also which direction water should bounce off of them. The main issue I'm sure is that the land tiles are on top of the water, so if I want to have the water elevation vary ... or put part of the tile underwater to run through the distortion shader now visible on my underwater tiles, I'll need an elegant solution or rework how my land tiles are integrated completely.

    • xyz replied to this.

      Erich_L When you start my game press the bottom-left blue button once. Is that what you mean by a procedural ramp texture? Or is that the bitmap?

      No, I meant this:

      void fragment() {
      	float velocity = 2.0;
      	float count = 5.0;
      	float stripe = fract( TIME * velocity + UV.x * count);
      	//stripe = step(.5, stripe);
      	ALBEDO = vec3(stripe);
      }

      You can step() or smoothstep() this into stripes of any thickness, orient them to move into direction of flow (via uv rotation) as real waves would, and additionally displace them in various ways. All algorithmically in the shader, without bitmap textures, resolution independent.

      Erich_L If I understand you correctly, I can also rotate the UVs which could alleviate the UV-mismatch between water tiles. I'd like to learn how to do that so I'll be studying your code tonight I'm sure for more than a few hours XD.

      I think you can't fix flow discontinuities solely by uv manipulation. You'll need to do tile blending as described in links you posted before. But this can be done at a later time regardless of how you draw tiles, procedurally or from bitmap textures.
      My suggestion was to use procedurals for more control and variety, to better show the direction of flow since it's not only an effect but also communicates gameplay relevant information.
      Dark lines you used do mask abrupt flow differences but this also strengthens the crude tiled look. Ok if that's what you're after visually but water would look more pleasing if tile borders are not pronounced, especially if you plan to add different level skins, with organic underwater textures, rocks or stuff like this. I'd surely consider implementing the blending. Leve it for after the gameplay prototype is done though.

      Erich_L For the border effect you presented, that seems like a really smart way to do it, but I feel like I'll need to send in a another texture to the shader to know where the land tiles are... and also which direction water should bounce off of them.

      You can put it in the flow texture, there's plenty of space there. Only two floats are used for the flow vector, there are two more left. Just 4 bits of information per tile is needed to tell the shader about surrounding coast configuration. This is easily packed into one float. Shader can then produce a fallof gradient for each existing coast edge, mix them together and use that to drive the intensity of the "foam" effect. I used the simplest approach to just drive the step argument that produces stripes from the above depicted gradient. There is no actual bouncing in my example, just thicker stripes as we approach the coast, keeping the direction of flow for the tile. Depending on art direction this may be sufficient. However superimposing additional bounce stripes is no problem once you calculate the coast falloff gradient.

        xyz I would certainly prefer to do it procedurally and I like your method more. It took me awhile to just figure out your vocabulary XD. I am ashamed I didn't think to put direction in radians and send it that way- I was using red and green for that. I put my direction in radians and tried your method and got these results.

        Edit: image fixed

        A scalar of 5.0 on velocity I can understand. (though I usually think of Velocity as a vec2 so that confused me, I called mine length.) I don't understand tho the scalar 6.0 on direction.

        • xyz replied to this.

          Erich_L You can encode flow information in at least two ways. In the example, I used the sillier one (angle and velocity scalar) because my flow texture was just some dummy noise. The purist way would be to send the velocity vector. It's the same information just represented differently. Shader can then use vector's length as velocity magnitude and its normalized x and y components directly as sine and cosine values for the rotation matrix. I suspect you were already doing it like that and my flow "encoding" in the example slightly confused you 🙂 But this is not really relevant. Shader just needs flow direction/magnitude data in one form or another.

          And yeah, scalar direction is just the direction angle in radians.

          It's hard to tell what's happening from that screenshot. Maybe make a simple black and white version of the shader, without the underwater part.

            xyz Problem turned out to be what @cybereality had mentioned earlier; one cannot simply just use texture(). For this stuff it needs to be texelFetch:

            	ivec2 cd = ivec2(floor(UV * tileMapSize)); 
            	vec4 flowColor = texelFetch(vectors, cd, 0);

            With that change I can get your method to work and I see it has the same problem really as mine- need to blend the sides together quite a bit.

            I think the simplest solution to my water elevation problem is simply to cut the tilemap into two textures and two nodes; one storing the tops and one storing the sides. I could then easily place the sides under the water shader and add a distortion effect.
            Edit: I updated my image in the last post to show ya

            • xyz replied to this.

              That updated image is looking as expected. Now it's just a matter of adjusting gradient and noise frequencies... and possibly implement tile blending.

              Erich_L Problem turned out to be what @cybereality had mentioned earlier; one cannot simply just use texture(). For this stuff it needs to be texelFetch

              You can use texture() if you have a large texture in which each tile is not just one pixel but say area of 32x32 pixels with same value, which is what I probably did. Sorry, I made this example in a rush just to demonstrate the approach, didn't take all practical problems into consideration.

              Erich_L With that change I can get your method to work and I see it has the same problem really as mine- need to blend the sides together quite a bit

              Using more high frequency displacement noise can mask some of it as you can see in my example, but only blending can make it smooth.

              @xyz I don't suppose you know how to get your wave effect to push in all directions outward from the center?
              Didn't quite get it on my own but I did do something!! That took me so long I'm embarrassed. Made you guys a hard level to beat tho @cybereality.

              Click to reveal Click to hide
              	float direction = 1.57 * 1.0;
              	mat2 m; // uv rotation matrix
              	m[0] = vec2(cos(direction), -sin(direction));
              	m[1] = vec2(sin(direction), cos(direction));
              	vec2 new_uv = m * gridUV;
              	new_uv.x += TIME * mag - new_uv.x*7.0;  // translate and scale uv
              	new_uv -= velocity.x;
              	new_uv.y += 0.5 * isTerrain;
              	float distUV = 1.0-distance(gridUV, vec2(0.5, 0.5));
              
              	new_uv.x += (texture(noise_texture, gridUV ).r )* 1.6; // noise uv
              	float f = fract(new_uv.x ) * distUV ;
              	f = step(0.5, f);
              	vec4 noiseColor = vec4(f,f,f,f);
              • xyz replied to this.

                Ok. So here's the whole thing, sans the foam. Blending looks best if the final output is used as a normal map.
                Flow map and animated procedural gradient tiles:

                Displacement noise added to gradients and 4-tile weighted blending. Noise texture is Godot's procedural simplex noise.

                Normal map calculated from gradient derivatives using shader's dFdx and dFdy functions:

                Final shader:

                This is just to demonstrate the whole approach. It can obviously be tweaked and improved.
                Here's the shader code:

                shader_type spatial;
                render_mode async_visible,blend_mix,depth_draw_opaque,cull_back,diffuse_burley,specular_schlick_ggx;
                uniform sampler2D texture_flow : hint_albedo;
                uniform sampler2D texture_noise : hint_albedo;
                uniform float wave_count : hint_range(1.0, 10.0, .5) = 3.0;
                uniform float noise_strength : hint_range(0.0, 1.0, .05) = .4;
                uniform float blend_zone : hint_range(.5, 1.0, .05) = 1.0;
                
                // flow texture sampling function
                vec2 sample_flow(ivec2 tile){
                	return texelFetch(texture_flow, clamp(tile, ivec2(0,0), textureSize(texture_flow, 0)), 0).rg;
                }
                
                
                // generate gradient pixel at given uv for given flow
                // gradient used here is smoothstepped. We can use linear or sine gradients as well.
                float wave_amplitude(vec2 flow, vec2 uv, float count){
                	float vel = length(flow)* 3.0 + 1.0; // velocity
                	vec2 dir = normalize(flow); 
                	uv = mat2(vec2(dir.y, -dir.x), dir) * uv; // direction - rotate via 2x2 matrix
                	float grad = fract( TIME * vel + uv.x * count); // translate
                	return smoothstep(0.0, .5, grad) * smoothstep(1.0, .5, grad); // smoothstep the gradient
                }
                
                
                void fragment() {
                	ROUGHNESS = 0.4;
                	METALLIC = .5;
                	SPECULAR = .5;
                	
                	vec2 uvtex = UV * vec2(textureSize(texture_flow, 0)); // texture coords in flowmap space
                	ivec2 tile = ivec2(uvtex); // tile index
                	vec2 tile_fract = fract(uvtex); // tile fraction
                
                	uvtex += texture(texture_noise, UV).rg * noise_strength; // uv noise
                	
                	// sample 4 nearest tiles and do weighted blending
                	vec2 baseTile = floor(uvtex - vec2(.5)) + vec2(.5);
                	float a = 0.0;
                	float w_total = 0.0;
                	for(int j = 0; j<=1; ++j){
                		for(int i = 0; i<=1; ++i){
                			float d = length(uvtex - (baseTile + vec2(float(i), float(j)))); 
                			float w = blend_zone - clamp(d*d, 0.0, blend_zone);
                			a += wave_amplitude(sample_flow(ivec2(floor(baseTile)) + ivec2(i, j)), uvtex, wave_count) * w;
                			w_total += w;
                		}	
                	}
                	a /= w_total;
                
                	// calculate normal
                	vec3 nmap = vec3(dFdx(a), dFdy(a), .3);
                	nmap = normalize(nmap);
                	NORMALMAP = nmap;
                	
                	// some albedo
                	ALBEDO = vec3(0.0, 0.2, 1.0);
                }

                  My mind is blown. I'm so jealous of your ability to throw shaders together out of thin air.

                  xyz Great result, congratulations.
                  In addition to just the shader code, a full demo scene would be very welcome (maybe on a github project for instance) as shader mostly depend on inputs and how their outputs are used, scene setup is very critical.

                    JusTiCe8 There's also godotshaders.com that the shader can be shared on and the github repo linked from.