Transparent Water Shader supporting SSR and Refraction
This intends to solve the problem that the SSR (Screen Space Reflection) implementation of Godot 4.X is limited to surfaces that are not transparent. For a water shader it is often necessary to support both transparency and reflections. In a lot of cases this can be worked around with reflection probes, but the need for SSR stands, especially because SSR works quiet well for water. The only solution is to write a custom SSR shader for that matter.
This is a full water shader implementation supporting:
- Screen Space Reflection
- Transparency
- 3D waves
- Edge detection
- Normal textures for a varied surface
- Fake Refraction (might not work for specific geometry, I think of finding a better solution)
- Fake underwater fog
What is missing:
- More advanced 3D wave physics
- Buoyancy API
- Objects creating wave ripples while going through the water
Performance
Should run well in most cases. I’ve tested it at 60fps on the Steam Deck at an SSR resolution of 2.0 without any other optimizing and the demo is out of the box rock steady at 60fps at 50% GPU with 2.3 watts.
Parameters
Setting | Description |
Color Shallow | The color that is shown in shallow water (and is mixed with visible objects beneath the surface, when transparency is > 0.0) |
Color Deep | The color that is shown in deep water. |
Transparency | The transparency of the water. |
Metallic | The metallic property of the water. |
Roughness | The roughness property of the water. |
Max Visible Depth | The depth when only color deep is rendered. |
Wave A Move Direction | The 2D vector of the Wave A texture. |
Wave B Move Direction | The 2D vector of the Wave B texture. |
Wave Noise Scale A | Scale of the 3D waves interpretation for wave A. Higher means wider waves. |
Wave Noise Scale B | Scale of the 3D waves interpretation for wave B. Higher means wider waves. |
Wave Time Scale A | The wave A speed. |
Wave Time Scale B | The wave B speed. |
Wave Height Scale | The y-height of waves interpreted from the wave textures. |
Wave Normal Flatness | The flatness of the new calculated normals. Higher values are smoother. |
Surface Normals Move Direction A | The 2D vector of the Surface Normals A texture. For details on the water surface. |
Surface Normals Move Direction B | The 2D vector of the Surface Normals B texture. This is merged with the details from A. |
Surface Texture Roughness | How rough the detail normals will look. |
Surface Texture Scale | Scale of the texture interpretation. Higher means smaller details. |
Surface Texture Time Scale | The speed the details move across the surface. |
SSR Resolution | The distance the ray-marching algorithm moves per step. Smaller values are better but slower. |
SSR Max Travel | The maximum distance the ray will travel before giving up. Longer is better but slower. 0 deactivates SSR. |
SSR Max Diff | The maximum diff from geometry that is counted as a ray-march hit. No performance impact. Low values might miss geometry, high values might create false positives. Aim for good looks. |
SSR Mix Strength | How visible the reflections are on the surface of the water. |
SSR Screen Border Fadeout | Strength of fade-out effect on reflections close to the screen borders |
Refraction Intensity | The intensity of the refraction effect. 0 is no refraction at all. |
Border Color | The coastal border color. This is the foam around objects in the water. |
Border Scale | The size of the coastal border. |
Border Near | Near plane for linear depth calculation for coastal borders. Good values improve the precision. Should be < Border Far. (Default: 0.5) |
Border Far | Far plane for linear depth calculation for coastal borders. Good values improve the precision depending on your scene scale. Should be > Border Near. (Default: 300.0) |
Cut Out X | The cut out in X direction. No pixels are rendered here. This can be useful, if an object resides in the middle that should not have a water surface inside. |
Cut Out Y | The cut out in Y direction. No pixels are rendered here. This can be useful, if an object resides in the middle that should not have a water surface inside. |
Wave A | The height texture for the wave layer A. Should be seamless and black and white, can be noise. |
Wave B | The height texture for the wave layer B. Should be seamless and black and white, can be noise. |
Surface Normals A | The normals texture with the surface normals for A. Should be seamless, can be noise. |
Surface Normals B | The normals texture with the surface normals for A. Should be seamless, can be noise. |
Shader code
shader_type spatial;
render_mode depth_draw_always;
const float EPSILON = 1e-5;
uniform sampler2D SCREEN_TEXTURE : hint_screen_texture, filter_nearest;
uniform sampler2D DEPTH_TEXTURE : hint_depth_texture, filter_nearest;
uniform vec3 color_shallow : source_color = vec3(0.01, 0.2, 0.3);
uniform vec3 color_deep : source_color = vec3(0.3, 0.5, 0.6);
uniform float transparency : hint_range(0.0, 1.0, 0.01) = 0.6;
uniform float metallic : hint_range(0.0, 1.0, 0.01) = 0.0;
uniform float roughness : hint_range(0.0, 1.0, 0.01) = 0.25;
uniform float max_visible_depth : hint_range(0.1, 100.0, 0.1) = 20.0;
uniform sampler2D wave_a;
uniform sampler2D wave_b;
uniform vec2 wave_move_direction_a = vec2(-1.0, 0.0);
uniform vec2 wave_move_direction_b = vec2(0.0, 1.0);
uniform float wave_noise_scale_a = 15.0;
uniform float wave_noise_scale_b = 15.0;
uniform float wave_time_scale_a = 0.15;
uniform float wave_time_scale_b = 0.15;
uniform float wave_height_scale = 1.0;
uniform float wave_normal_flatness : hint_range(0.1, 100.0, 0.1) = 50.0;
uniform sampler2D surface_normals_a;
uniform sampler2D surface_normals_b;
uniform vec2 surface_normals_move_direction_a = vec2(-1.0, 0.2);
uniform vec2 surface_normals_move_direction_b = vec2(0.2, 1.0);
uniform float surface_texture_roughness : hint_range(0.0, 1.0, 0.01) = 0.15;
uniform float surface_texture_scale : hint_range(0.001, 2.0, 0.001) = 0.1;
uniform float surface_texture_time_scale : hint_range(0.001, 2.0, 0.001) = 0.06;
uniform float ssr_resolution : hint_range(0.01, 10.0, 0.1) = 1.0;
uniform float ssr_max_travel : hint_range(0.0, 200.0, 0.1) = 30.0;
uniform float ssr_max_diff : hint_range(0.1, 10.0, 0.1) = 4.0;
uniform float ssr_mix_strength : hint_range(0.0, 1.0, 0.01) = 0.7;
uniform float ssr_screen_border_fadeout: hint_range(0.0, 1.0, 0.1) = 0.3;
uniform float refraction_intensity : hint_range(0.0, 1.0, 0.01) = 0.4;
uniform vec3 border_color : source_color = vec3(1.0);
uniform float border_scale : hint_range(0.0, 5.0, 0.01) = 2.0;
uniform float border_near = 0.5;
uniform float border_far = 300.0;
uniform float cut_out_x = 0.0;
uniform float cut_out_z = 0.0;
varying vec2 vertex_uv;
varying vec3 local_position;
float get_wave_height(vec2 uv_a, vec2 uv_b)
{
float height1 = texture(wave_a, uv_a).y;
float height2 = texture(wave_b, uv_b).y;
return (height1 + height2) / 2.0;
}
vec3 get_mixed_normals(vec3 color1, vec3 color2)
{
vec3 normal1 = normalize(color1 * 2.0 - 1.0);
vec3 normal2 = normalize(color2 * 2.0 - 1.0);
vec3 up = vec3(0.0, 0.0, 1.0);
vec3 tangent = normalize(normal1 + normal2);
vec3 binormal = normalize(cross(up, tangent));
vec3 mixedNormal = normalize(cross(tangent, binormal));
return mixedNormal * 0.5;
}
void vertex()
{
local_position = VERTEX;
vertex_uv = (MODEL_MATRIX * vec4(local_position, 1.0)).xz;
vec2 uv_a = vertex_uv / wave_noise_scale_a + (wave_move_direction_a * TIME * wave_time_scale_a);
vec2 uv_b = vertex_uv / wave_noise_scale_b + (wave_move_direction_b * TIME * wave_time_scale_b);
VERTEX.y += get_wave_height(uv_a, uv_b) * wave_height_scale;
float normal_height_scale = wave_height_scale / wave_normal_flatness;
vec2 e = vec2(0.01, 0.0);
vec3 normal = normalize(vec3(
get_wave_height(uv_a - e, uv_b - e) * normal_height_scale - get_wave_height(uv_a + e, uv_a + e) * normal_height_scale,
2.0 * e.x,
get_wave_height(uv_a - e.yx, uv_b - e.yx) * normal_height_scale - get_wave_height(uv_a + e.yx, uv_b + e.yx) * normal_height_scale
));
NORMAL = normal;
}
bool is_within_screen_boundaries(vec2 position)
{
return position.x > 0.0 && position.x < 1.0 && position.y > 0.0 && position.y < 1.0;
}
vec2 get_uv_from_view_position(vec3 position_view_space, mat4 proj_m)
{
vec4 position_clip_space = proj_m * vec4(position_view_space.xyz, 1.0);
vec2 position_ndc = position_clip_space.xy / position_clip_space.w;
return position_ndc.xy * 0.5 + 0.5;
}
vec3 get_view_position_from_uv(vec2 uv, float depth, mat4 inv_proj_m)
{
vec4 position_ndc = vec4((uv * 2.0) - 1.0, depth, 1.0);
vec4 view_position = inv_proj_m * position_ndc;
return view_position.xyz /= view_position.w;
}
bool is_zero(float value)
{
return abs(value) < EPSILON;
}
float get_screen_border_alpha(vec2 screen_position)
{
vec2 shifted_screen_position = 4.0 * screen_position * (1.0 - screen_position);
float mask = shifted_screen_position.x * shifted_screen_position.y; // ranging from 0.0 at the edges to 1.0 in the center
// An offset in the [0.0, 0.5] range for ssr_screen_border_fadeout values > 0.75
// which is subtracted from the result of smoothstep.
// This ensure alpha smoothly transitions to zero when ssr_screen_border_fadeout is approaching 1.0.
float offset = mix(0.0, 0.5, (clamp(ssr_screen_border_fadeout, 0.75, 1.0)-0.75) / 0.25);
float alpha = clamp(smoothstep(0.0, 2.0 * ssr_screen_border_fadeout, mask) - offset, 0.0, 1.0);
return is_zero(ssr_screen_border_fadeout) ? 1.0 : alpha;
}
vec4 get_ssr_color(vec3 surface_view_position, vec3 normal_view_space, vec3 view_view_space, mat4 proj_m, mat4 inv_proj_m)
{
if (ssr_max_travel < EPSILON)
{
return vec4(0);
}
vec3 current_position_view_space = surface_view_position;
vec3 view_direction_view_space = view_view_space * -1.0;
vec3 reflect_vector_view_space = normalize(reflect(view_direction_view_space.xyz, normal_view_space.xyz));
vec2 current_screen_position = vec2(0.0);
vec3 resulting_color = vec3(-1.0);
for(float travel=0.0; resulting_color.x < 0.0 && travel < ssr_max_travel; travel = travel + ssr_resolution)
{
current_position_view_space += reflect_vector_view_space * ssr_resolution;
current_screen_position = get_uv_from_view_position(current_position_view_space, proj_m);
float depth_texture_probe_raw = texture(DEPTH_TEXTURE, current_screen_position).x;
vec3 depth_texture_probe_view_position = get_view_position_from_uv(current_screen_position, depth_texture_probe_raw, inv_proj_m);
float depth_diff = depth_texture_probe_view_position.z - current_position_view_space.z;
vec3 ssr_screen_color = texture(SCREEN_TEXTURE, current_screen_position.xy).rgb;
resulting_color = (is_within_screen_boundaries(current_screen_position) && depth_diff >= 0.0 && depth_diff < ssr_max_diff) ? ssr_screen_color : vec3(-1.0);
}
float alpha = get_screen_border_alpha(current_screen_position);
return vec4(resulting_color,alpha);
}
float linear_depth(float cur_depth)
{
return border_far * border_near / (border_near + cur_depth * (border_far - border_near));
}
float normalize_float(float min_v, float max_v, float value)
{
float clamped_value = clamp(value, min_v, max_v);
return (clamped_value - min_v) / (max_v - min_v);
}
vec2 get_refracted_uv(vec2 raw_screen_uv, float screen_depth_raw, vec3 view, vec3 normal, mat4 proj_m, mat4 inv_proj_m)
{
vec3 screen_view_position_original = get_view_position_from_uv(raw_screen_uv, screen_depth_raw, inv_proj_m);
float screen_center_distance = clamp(abs(length(raw_screen_uv - vec2(0.5, 0.5))) * 2.0, 0.0, 1.0);
float refraction_intensity_deglitched = mix(1.0 - refraction_intensity, 1.0, screen_center_distance);
vec3 refraction_position_view_space = screen_view_position_original + normalize(refract(view, -normal, refraction_intensity_deglitched));
vec2 refraction_uv = get_uv_from_view_position(refraction_position_view_space, proj_m);
return refraction_uv;
}
void fragment()
{
vec3 normal = NORMAL;
float screen_depth_raw = texture(DEPTH_TEXTURE, SCREEN_UV).x;
vec2 refraction_uv = refraction_intensity > 0.0 ? get_refracted_uv(SCREEN_UV, screen_depth_raw, VIEW, normal, PROJECTION_MATRIX, INV_PROJECTION_MATRIX) : SCREEN_UV;
float screen_depth = texture(DEPTH_TEXTURE, refraction_uv).x;
float surface_depth = FRAGCOORD.z;
float border_diff = linear_depth(screen_depth_raw) - linear_depth(surface_depth);
vec2 time_vector = (TIME * surface_normals_move_direction_a) * surface_texture_time_scale;
vec2 time_vector2 = (TIME * surface_normals_move_direction_b) * surface_texture_time_scale;
vec3 normal_texture_blend = get_mixed_normals(texture(surface_normals_a, vertex_uv * surface_texture_scale + time_vector).xyz, texture(surface_normals_b, vertex_uv * surface_texture_scale + time_vector2).xyz);
vec3 normal_blend = mix(normal, normal_texture_blend, surface_texture_roughness);
vec3 screen_view_position = get_view_position_from_uv(refraction_uv, screen_depth, INV_PROJECTION_MATRIX);
vec3 surface_view_position = get_view_position_from_uv(SCREEN_UV, surface_depth, INV_PROJECTION_MATRIX);
float depth_visibility = 1.0 - normalize_float(0.0, max_visible_depth, length(surface_view_position - screen_view_position));
vec3 screen_color = texture(SCREEN_TEXTURE, refraction_uv).rgb;
vec4 ssr_color = get_ssr_color(surface_view_position, normal, VIEW, PROJECTION_MATRIX, INV_PROJECTION_MATRIX);
vec3 surface_color_transparency = mix(color_shallow, screen_color, transparency);
vec3 surface_depth_color_mix = mix(color_deep, surface_color_transparency, depth_visibility);
vec3 surface_color_ssr_mix = (ssr_max_travel > EPSILON) ? mix(surface_depth_color_mix, ssr_color.rgb, ssr_mix_strength * ssr_color.a) : vec3(0);
vec3 water_color = (ssr_color.x >= 0.0) ? surface_color_ssr_mix : surface_depth_color_mix;
vec3 final_color = mix(border_color, water_color, step(border_scale, border_diff));
float cut_out_x_half = cut_out_x / 2.0;
float cut_out_z_half = cut_out_z / 2.0;
if((local_position.x < cut_out_x_half && local_position.x > -cut_out_x_half) &&
(local_position.z < cut_out_z_half && local_position.z > -cut_out_z_half))
{
discard;
}
ALBEDO.rgb = final_color;
METALLIC = metallic;
ROUGHNESS = roughness;
NORMAL = normal_blend;
}
Please, update de video link.