Rain puddles with screen space reflection

This shader is a follow-up on the world normals post-processing shader I made recently (link).

I took the implementation of the SSR shader made by marcelb (link) and modified the code to apply the reflections globally as a post-processing effect.

Puddle locations are generated based on normals (only surfaces pointing up recieve the reflections) and two noise masks that take pixel world position as input.

The result is not perfect but I’m happy to share it as a baseline for any reflection related post-processing effects. Feel free to copy and modify the code. If you want to add waves to the reflections, modify “normal” at line 146 by adding any offset. If you want pure reflections on all surfaces, read the line 158.

Important! Requires post-processing scene setup using quad mesh (link to official docs):

  1. Create MeshInstance with “Quad” mesh and set its size to 2×2 m and enable “Flip Faces”.
  2. Set MeshInstance “extra_cull_margin” to maximum (16k) and disable “Cast Shadow”.
  3. Set the created quad mesh as a child of your main camera.
  4. Create Shader resource and copy the code.
  5. Create ShaderMaterial resource, set the shader and attach the material to the MeshInstance.

 

Shader code
// Rain puddles using SSR
// CC0, shadecore_dev, 2025.
// Includes parts of the SSR implementation created by marcelb: https://godotshaders.com/shader/transparent-water-shader-supporting-ssr/

shader_type spatial;
render_mode unshaded, fog_disabled;

const float EPSILON = 1e-5;

uniform float puddle_reflectivness = 2.0;

uniform vec3 puddle_color : source_color = vec3(0.01, 0.3, 0.28);
uniform float puddle_color_alpha = 0.7;

uniform sampler2D depth_texture : hint_depth_texture;
uniform sampler2D screen_texture : hint_screen_texture, filter_nearest;

uniform float puddle_micro_mask_size = 5.0;
uniform float puddle_macro_mask_size = 25.0;

uniform float puddle_mask_threshold = 0.33;

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;

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_within_screen_boundaries(vec2 position) 
{
	return position.x > 0.0 && position.x < 1.0 && position.y > 0.0 && position.y < 1.0;
}

bool is_zero(float value) 
{
    return abs(value) < EPSILON;
}

vec2 random(vec2 uv){
    uv = vec2( dot(uv, vec2(127.1,311.7) ),
               dot(uv, vec2(269.5,183.3) ) );
    return -1.0 + 2.0 * fract(sin(uv) * 43758.5453123);
}

float noise(vec2 uv) {
    vec2 uv_index = floor(uv);
    vec2 uv_fract = fract(uv);

    vec2 blur = smoothstep(0.0, 1.0, uv_fract);

    return mix( mix( dot( random(uv_index + vec2(0.0,0.0) ), uv_fract - vec2(0.0,0.0) ),
                     dot( random(uv_index + vec2(1.0,0.0) ), uv_fract - vec2(1.0,0.0) ), blur.x),
                mix( dot( random(uv_index + vec2(0.0,1.0) ), uv_fract - vec2(0.0,1.0) ),
                     dot( random(uv_index + vec2(1.0,1.0) ), uv_fract - vec2(1.0,1.0) ), blur.x), blur.y) + 0.5;
}


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;
	
	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);
}

void vertex() {
	POSITION = vec4(VERTEX.xy, 1.0, 1.0);
}

vec3 reconstruct_world_position(vec2 uv, float depth, mat4 inv_proj_matrix, mat4 inv_view_matrix) {
	#if CURRENT_RENDERER == RENDERER_COMPATIBILITY
	vec3 ndc = vec3(uv, depth) * 2.0 - 1.0;
	#else
	vec3 ndc = vec3(uv * 2.0 - 1.0, depth);
	#endif

	vec4 view = inv_proj_matrix * vec4(ndc, 1.0);
	view.xyz /= view.w;

	vec4 world = inv_view_matrix * inv_proj_matrix * vec4(ndc, 1.0);
	return world.xyz / world.w;
}

void fragment() {
	vec2 uv_center = SCREEN_UV;
	vec2 uv_right = SCREEN_UV + vec2(1, 0) / VIEWPORT_SIZE;
	vec2 uv_top = SCREEN_UV + vec2(0, 1) / VIEWPORT_SIZE;

	float depth_center = texture(depth_texture, uv_center).x;
	float depth_right = texture(depth_texture, uv_right).x;
	float depth_top = texture(depth_texture, uv_top).x;

	vec3 center = reconstruct_world_position(uv_center, depth_center, INV_PROJECTION_MATRIX, INV_VIEW_MATRIX);
	vec3 right = reconstruct_world_position(uv_right, depth_right, INV_PROJECTION_MATRIX, INV_VIEW_MATRIX);
	vec3 top = reconstruct_world_position(uv_top, depth_top, INV_PROJECTION_MATRIX, INV_VIEW_MATRIX);

	vec3 normal = normalize(cross(top - center, right - center));
	
	vec3 normal_view_space = (VIEW_MATRIX * vec4(normal, 0.0)).xyz;
	
	vec3 view_view_space = normalize(-get_view_position_from_uv(SCREEN_UV, 1.0, INV_PROJECTION_MATRIX));
	
	vec3 surface_view_position 	= get_view_position_from_uv(SCREEN_UV, depth_center, INV_PROJECTION_MATRIX);

	vec4 ssr = get_ssr_color(surface_view_position, normal_view_space, view_view_space, PROJECTION_MATRIX, INV_PROJECTION_MATRIX);

	vec3 surface_color_ssr_mix = (ssr_max_travel > EPSILON) ? mix(vec3(0.0), ssr.rgb, ssr_mix_strength * ssr.a) : vec3(0);
	
	// Uncomment 2 lines below and comment everything underneath for pure reflections without masking.
	// ALBEDO = ssr.rgb;
	// ALPHA = ssr.x < 0.0 ? 0.0 : ssr.a;
	
	float puddle_mask_micro =  noise(center.xz / puddle_micro_mask_size);
	float puddle_mask_macro = noise(center.xz / puddle_macro_mask_size);
	
	float final_puddle_mask = clamp(puddle_reflectivness * puddle_mask_micro * clamp(puddle_mask_macro, 0.0, 1.0) * normal.y - puddle_mask_threshold, 0.0, 1.0);
	
	ALBEDO = mix(surface_color_ssr_mix, puddle_color, float(ssr.x < 0.0));
	
	ALPHA = mix(0.0, ssr.x < 0.0 ? puddle_color_alpha : max(ssr.a, puddle_color_alpha), final_puddle_mask);
}
Tags
3d, postprocess, SSR
The shader code and all code snippets in this post are under CC0 license and can be used freely without the author's permission. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

More from shadecore_dev

Pixel dotted silhouette

Gradient pixel glint

Rain puddles with ripples and reflections

Related shaders

Rain puddles with ripples and reflections

Universal Screen Space Reflection(SSR)屏幕空间反射 use ray marching

Screen Space Mesh Projection

guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments