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.
Image Color Data to Godot Shader
- Edited
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.
- Edited
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.
looks psychedelic
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