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.

                  I have also learned from this that you cannot depend on the shader getting vec4s from the image that have floats out of the color range 0 to 1.0. On PC it's fine but on HTML my 2.0 passed in the green channel was squashed to 1. Just something to be aware of.

                    Erich_L The target renderer probably doesn't support float textures so it defaulted to standard RGBA8 format, with one byte of information per channel per pixel. If you want a bulletproof version, determine the maximal flow velocity (say 10) and then remap the actual vector components from range [-10.0, 10.0] to range [0.0, 1.0] prior to writing them into image. In the shader just map the texture readout back into [-10.0, 10.0] range. You'll lose some precision with only one byte of information per vector component, but I think it'll still be good enough for this effect.

                    @JusTiCe8 @Megalomaniak This shader is really just a quickly hacked demo. The code is not general enough nor tested enough to be published as a ready-to-use shader. But if anyone wants to polish it up and put it on the web - you're welcome. The setup is really quite simple. There are only two input textures: flowmap with velocity encoded into rg channels and optional noise texture (I used Godot's simplex noise). For the flowmap I just made a plain pixel noise in photoshop. Erich's got an actual flowmap generator though.

                    Btw @Erich_L Your game is 2d, right? The example is made with the spatial shader but exactly the same thing can be done in the canvas item shader. You'll just need to do a simple lighting calculation using the computed normal, or even make a lookup into an environment texture for some nice fake reflections. The wave heightmap generated by the shader (one step prior to calculating the normal) can also be used in lots of other ways for various effects. It really depends on visual style you're after.

                      Erich_L I have also learned from this that you cannot depend on the shader getting vec4s from the image that have floats out of the color range 0 to 1.0. On PC it's fine but on HTML my 2.0 passed in the green channel was squashed to 1. Just something to be aware of.

                      Probably should normalize the data, can probably have a velocity/speed multiplier uniform/parameter modulate the values later in the pipeline.

                      xyz @Megalomaniak Yes I had no problem normalizing the data to the color range, I just found it interesting some devices will let you pass in values outside the color range.

                      xyz: This shader is really just a quickly hacked demo

                      For a hacked demo it's spectacular! If I put something like that to use I'd struggle to create any assets of matching quality. One thing I admit I miss from 3D is being able to download materials which came packed with normal and roughness maps ect. I suppose they could be worked into shaders or 2d materials, but at some point I wonder if you might as well be just starting from a 3D scene. My effect is fairly close to the original goal but I was able, so far, to adapt your method to creating waves near terrain. Also was able to save a color channel by converting my flow vectors to radians, so now my flow vector to color function looks like Color(radians, vector_length, free channel (yay), quack) where quack gives me information on whether or not there's a land tile.

                      The solution currently looks like this and while it's not super sleek and doesn't blend perfectly at tile edges, at the moment it fulfills the following: the player can clearly see

                      • flow direction
                      • changes in rate of flow
                      • objects under the water
                      • water-shore boundaries

                      The "flowmap generator" I have is a big mess of code that goes over the scene in multiple passes. It physically ails me to edit the terrain mid-game because I know exactly how many lines there are it takes to recompute the flow map. Among others, one thing I learned is that you can't blend water velocities near walls with neighbors unless you want debris to get stuck against walls.


                      Compare this withthe solution a week ago. I have had several disgusting moments this week alone and giddy with myself over this project.

                      Also, how cool would it be if you could apply a shader to floating objects to use scrolling noise to subtract 1 from the z-index where n > 0 to easily make floating objects appear to be bobbing up and down in the water?