Image Color Data to Godot Shader
Asked Answered
I

33

0

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?

Iniquity answered 3/3, 2023 at 6:40 Comment(0)
J
0

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);
}
Jerboa answered 21/3, 2023 at 2:25 Comment(0)
I
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.

Iniquity answered 4/3, 2023 at 18:27 Comment(0)
K
0

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.

Krystalkrystalle answered 5/3, 2023 at 3:39 Comment(0)
I
0

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.

Iniquity answered 16/3, 2023 at 16:47 Comment(0)
J
0

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:

Jerboa answered 16/3, 2023 at 22:13 Comment(0)
I
0

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;
}
Iniquity answered 18/3, 2023 at 2:23 Comment(0)
I
0

Jerboa 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.

Iniquity answered 18/3, 2023 at 2:33 Comment(0)
J
0

Iniquity 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.

Iniquity 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.

Iniquity 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.

Jerboa answered 18/3, 2023 at 13:3 Comment(0)
I
0

Jerboa 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.

Iniquity answered 20/3, 2023 at 14:16 Comment(0)
J
0

Iniquity 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.

Jerboa answered 20/3, 2023 at 14:55 Comment(0)
I
0

Jerboa Problem turned out to be what Midstream 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

Iniquity answered 20/3, 2023 at 15:2 Comment(0)
J
0

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

Jerboa answered 20/3, 2023 at 15:11 Comment(0)
J
0

Iniquity 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.

Jerboa answered 20/3, 2023 at 15:18 Comment(0)
J
0

Iniquity 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.

Jerboa answered 20/3, 2023 at 15:22 Comment(0)
I
0

Casting 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 Midstream.

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);
Iniquity answered 20/3, 2023 at 18:39 Comment(0)
J
0

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);
}
Jerboa answered 21/3, 2023 at 2:25 Comment(0)
K
0

BLACK MAGIC!!!11 Burn the witch.

Krystalkrystalle answered 21/3, 2023 at 4:8 Comment(0)
I
0

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

Iniquity answered 21/3, 2023 at 13:2 Comment(0)
G
0

Jerboa 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.

Gove answered 21/3, 2023 at 13:49 Comment(0)
C
0

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

Crichton answered 21/3, 2023 at 14:57 Comment(0)
I
0

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.

Iniquity answered 21/3, 2023 at 21:4 Comment(0)
J
0

Iniquity 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.

Fanciful Plead 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 Goldstone 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.

Jerboa answered 22/3, 2023 at 19:21 Comment(0)
C
0

Iniquity 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.

Crichton answered 22/3, 2023 at 23:34 Comment(0)
I
0

Jerboa Plead 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.

Iniquity answered 23/3, 2023 at 11:43 Comment(0)
I
0

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?

Iniquity answered 23/3, 2023 at 12:18 Comment(0)
C
0

Iniquity 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.

Crichton answered 23/3, 2023 at 15:0 Comment(0)
J
0

Iniquity 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)) );
	
}
Jerboa answered 29/3, 2023 at 1:2 Comment(0)
C
0

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

Combs answered 29/3, 2023 at 2:10 Comment(0)
I
0

Casting This song started playing as soon as I laid eyes on this mind blowing development. TIME TO START UPGRADING. crackOpenEnergyDrink()

Iniquity answered 29/3, 2023 at 15:5 Comment(0)
J
0

Iniquity Thumbs up! Hope your dev skillz are better than your taste in music 😉

Jerboa answered 29/3, 2023 at 23:59 Comment(0)
I
0

Jerboa I believe uv = mat2(vec2(dir.y, -dir.x), dir) * uv; needs to be changed to
uv = mat2(vec2(-dir.y, dir.x), dir) * uv;

Here's where I got trying to make some small improvements. WARNING! View at your own risk.

Edit: I got most of the first post working (only) here.

Iniquity answered 30/3, 2023 at 20:32 Comment(0)
M
0

looks psychedelic 😉

Mouser answered 31/3, 2023 at 18:13 Comment(0)
I
0

Does anyone know if this process (making a flowmap and giving it to the shader) should be thread safe? I guess I'll find out XD

Iniquity answered 2/4, 2023 at 16:6 Comment(0)
J
0

Iniquity Should be fine. I used worker threads for building mesh data without problems. Making bitmaps is not much different.

Jerboa answered 3/4, 2023 at 13:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.