Transparent Water Shader supporting SSR and Refraction
This intends to solve the problem that the SSR (Screen Space Reflection) implementation of Godot 4.1 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)
What is missing:
- Over- to underwater transition
- 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 without any lowering of SSR resolution or any other optimizing and the demo is out of the box rock steady at 60fps at 50% GPU with 2.3 watts.
Parameters
Shader code
shader_type spatial;
uniform sampler2D SCREEN_TEXTURE : hint_screen_texture, filter_nearest;
uniform sampler2D DEPTH_TEXTURE : hint_depth_texture, filter_nearest;
uniform vec3 albedo : source_color = vec3(0.02, 0.45, 0.8);
uniform float transparency : hint_range(0.0, 1.0, 0.01) = 0.4;
uniform float metallic : hint_range(0.0, 1.0, 0.01) = 0;
uniform float roughness : hint_range(0.0, 1.0, 0.01) = 0.5;
uniform vec2 surface_normals_move_direction_a = vec2(-1.0, 0.0);
uniform vec2 surface_normals_move_direction_b = vec2(0.0, 1.0);
uniform float surface_texture_roughness : hint_range(0.0, 1.0, 0.01) = 0.6;
uniform float surface_texture_scale : hint_range(0.001, 2.0, 0.001) = 0.3;
uniform float surface_texture_time_scale : hint_range(0.001, 2.0, 0.001) = 0.06;
uniform float ssr_resolution : hint_range(0.1, 10.0, 0.1) = 2.0;
uniform float ssr_max_travel : hint_range(0.1, 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 wave_noise_scale = 15.0;
uniform float wave_height_scale = 0.25;
uniform float wave_time_scale = 0.025;
uniform float wave_normal_flatness : hint_range(0.1, 100.0, 0.1) = 30.0;
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 refraction_intensity : hint_range(0.0, 1.0, 0.01) = 0.4;
uniform float max_visible_depth : hint_range(0.0, 100.0, 0.1) = 10.0;
uniform vec3 color_deep : source_color = vec3(0.0, 0.1, 0.4);
uniform sampler2D wave_a;
uniform sampler2D wave_b;
uniform sampler2D surface_normals_a;
uniform sampler2D surface_normals_b;
varying vec2 vertex_uv;
float get_wave_height(vec2 uv)
{
float height1 = texture(wave_a, uv).y;
float height2 = texture(wave_b, uv).y;
return (height1 + height2) / 2.0;
}
void vertex()
{
vertex_uv = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xz;
vec2 uv = vertex_uv / wave_noise_scale + TIME * wave_time_scale;
VERTEX.y += get_wave_height(uv) * 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 - e) * normal_height_scale - get_wave_height(uv + e) * normal_height_scale,
2.0 * e.x,
get_wave_height(uv - e.yx) * normal_height_scale - get_wave_height(uv + 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;
}
vec3 get_ssr_color(vec3 surface_view_position, vec3 normal_view_space, vec3 view_view_space, mat4 proj_m, mat4 inv_proj_m)
{
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;
resulting_color = (is_within_screen_boundaries(current_screen_position) && depth_diff >= 0.0 && depth_diff < ssr_max_diff) ? texture(SCREEN_TEXTURE, current_screen_position.xy).rgb : vec3(-1.0);
}
return resulting_color;
}
float border(float cur_depth)
{
return border_near * border_far / (border_far + (2.0 * cur_depth - 1.0) * (border_near - border_far));
}
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() {
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 = border(screen_depth_raw) - border(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 = mix(texture(surface_normals_a, vertex_uv * surface_texture_scale + time_vector).xyz, texture(surface_normals_b, vertex_uv * surface_texture_scale + time_vector2).xyz, 0.5);
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(refraction_uv, surface_depth, INV_PROJECTION_MATRIX);
float depth_opacity = 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;
vec3 surface_color = albedo;
vec3 ssr_color = get_ssr_color(surface_view_position, NORMAL, VIEW, PROJECTION_MATRIX, INV_PROJECTION_MATRIX);
vec3 color_with_transparency = mix(surface_color, screen_color, transparency);
vec3 depth_color = mix(color_deep.rgb, color_with_transparency, depth_opacity);
vec3 water_color = (ssr_color.x >= 0.0) ? mix(depth_color, ssr_color, ssr_mix_strength) : depth_color;
vec3 final_color = mix(border_color, water_color, step(border_scale, border_diff));
ALBEDO.rgb = final_color;
METALLIC = metallic;
ROUGHNESS = roughness;
NORMAL_MAP = normal_blend;
}
Please, update de video link.