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?

                    Erich_L I'm not sure it'd work well with the z-index being an integral value. But maybe I'm wrong. Doing it as an additional effect on the buoying object itself in shader via scalar values would give a smoother result.

                    5 days later

                    Erich_L I don't suppose you know how to get your wave effect to push in all directions outward from the center?

                    I tinkered with this some more... just for fun. So here's a basic foam shader that pushes "away" from land tiles.

                    I needed to take care of your funky tile system (land tiles fusing when adjacent). This complicated the calculation a bit.
                    The only input is 1-pixel-per-tile texture, telling shader whether a tile is land or water. So just 1 bit of information per tile. I used Godot's simplex noise, thresholded by the shader.

                    Shader basically calculates a distance field for 4 nearest tiles and then uses the minimal one. All 8 adjacent tiles need to be sampled for each tile's distance field calculation. That's 9*4=36 texelFetch() lookups per pixel. This can be greatly optimized by sending adjacent tile information via the same input texture (so 9 bits of information then in total per tile). In that case only 4 texelFetch() calls are needed. Further optimization is possible if foam zone is confined only to 1 tile.

                    Calculated distance field is then remapped into animated stripes and composited over a simple water/land mix, using normalized distance itself as the opacity falloff factor. The shader also renders the tile grid.
                    Tile inset, foam zone width and number of stripes are controlled via uniforms.
                    Calculated distance field can be used in myriad other ways. The whole thing can of course be displaced by a hires noise texture for more watery look.

                    shader_type spatial;
                    render_mode blend_mix,depth_draw_opaque,cull_back,diffuse_burley,specular_schlick_ggx, unshaded;
                    uniform sampler2D tile_texture;
                    uniform float inset: hint_range(0.0, 1.0, .01) = .25;
                    uniform float foam_width: hint_range(0.0, 1.5, .05) = .6;
                    uniform float foam_steps: hint_range(0.0, 10.0, .1) = 3.0;
                    
                    
                    // sample tile texture, return 0.0 for water, 1.0 for land
                    float is_land(ivec2 tile){
                    	tile = clamp(tile, ivec2(0,0), textureSize(tile_texture,0) - ivec2(1,1) );  
                    	return step(.35, texelFetch(tile_texture, tile, 0).r);
                    }
                    
                    
                    // distance field of an origin centered box
                    float box_dist(vec2 halfsize, vec2 p){
                    	return length( max( abs(p)-halfsize, 0.0 ));
                    }
                    
                    
                    // calculate distance field for a single tile
                    float distance_from_land_in_tile(vec2 uv_tile, ivec2 tile){
                    	
                    	// distance of the current pixel from sampled tile center
                    	vec2 uv_from_tile_center =  uv_tile - vec2(tile) - vec2(.5);
                    
                    	// get land/water status of the tile and 8 adjacent tiles
                    	float land[9];
                    	int a = 0;
                    	for(int j = -1; j <= 1; ++j){
                    		for(int i = -1; i <= 1; ++i){
                    		land[a++] = is_land(tile + ivec2(i,j));
                    		}
                    	}
                    	
                    	// get distance of all possible sub-regions of a tile and return the minimum 
                    	float tilesize = 1.0-inset;
                    	float dist = foam_width;
                    	dist = mix(dist, min(dist, box_dist(vec2(tilesize)*.5, uv_from_tile_center) ), land[4] );
                    	dist = mix(dist, min(dist, box_dist(vec2(inset)*.5, uv_from_tile_center + vec2(.5)) ), land[0] * land[1] * land[3] * land[4] );
                    	dist = mix(dist, min(dist, box_dist(vec2(inset)*.5, uv_from_tile_center - vec2(.5)) ), land[5] * land[7] * land[8] * land[4] );
                    	dist = mix(dist, min(dist, box_dist(vec2(inset)*.5, uv_from_tile_center + vec2(.5, -.5)) ), land[3] * land[6] * land[7] * land[4] );
                    	dist = mix(dist, min(dist, box_dist(vec2(inset)*.5, uv_from_tile_center - vec2(.5, -.5)) ), land[1] * land[2] * land[5] * land[4] );
                    	dist = mix(dist, min(dist, box_dist(vec2(inset,tilesize)*.5, uv_from_tile_center + vec2(.5, 0.0))), land[3] * land[4] );
                    	dist = mix(dist, min(dist, box_dist(vec2(inset,tilesize)*.5, uv_from_tile_center - vec2(.5, 0.0))), land[5] * land[4] );
                    	dist = mix(dist, min(dist, box_dist(vec2(tilesize, inset)*.5, uv_from_tile_center + vec2(0.0, .5))), land[1] * land[4] );
                    	dist = mix(dist, min(dist, box_dist(vec2(tilesize, inset)*.5, uv_from_tile_center - vec2(0.0, .5))), land[7] * land[4] );
                    	return dist;
                    }
                    
                    
                    void fragment() {
                    
                    	vec2 uv_tile = UV*vec2(textureSize(tile_texture,0)); // uv coordinate in tile space
                    	ivec2 tile = ivec2(uv_tile); // tile index
                    	vec2 tile_fract = fract(uv_tile); // tile fraction
                    	
                    	// max possible distance
                    	float dist = foam_width;
                    	
                    	// get distance field of 4 nearest tiles and use the minimum.
                    	ivec2 base_tile = ivec2(uv_tile - vec2(.5));
                    	for(int j = 0; j <= 1; ++j){
                    		for(int i = 0; i <= 1; ++i){
                    			dist = min(dist, distance_from_land_in_tile(uv_tile, base_tile + ivec2(i, j)));
                    		}
                    	}
                    	
                    	// normalize distance into maximal range
                    	float dist_norm = mix(0.0, 1.0, dist/foam_width); 
                    	dist_norm = clamp(dist_norm, 0.0, 1.0);
                    	
                    	// foam gradients
                    	float foam_gradient =  step(.000001,dist_norm)*(1.0-dist_norm);
                    	float foam = step(.5, fract(dist_norm * foam_steps - TIME*1.5));
                    	foam = clamp(foam, 0.0, 1.0);
                    	
                    	// land/water mix
                    	ALBEDO = mix(vec3(0.01, .07, .04), vec3(0.0, .2, .37), step(.000001,dist_norm));
                    	// mix in foam
                    	ALBEDO = mix(ALBEDO, vec3(1.0), foam * foam_gradient);
                    	// debug grid
                    	ALBEDO = mix(ALBEDO, ALBEDO*.7, 1.0- vec3(step(.02,tile_fract.x) * step(.02, tile_fract.y)) );
                    	
                    }

                    I don't have a clue what I'm looking at, but it looks awesome.