Realistic Water with reflection and refraction

A realistic, controllable water shader with SSR, screen space refraction, fog, and Snell’s window.

Attach to a plane or quad, set height strength to 0 if you aren’t using subdivision.

This shader only works facing up.

 

Note: An edited version of the Parallax Mapping shader by Lohik is used in the background, and Stylized Sky by Gotibo is used for the sky

Also, the Snell’s window function is from Snell’s Window by tentabrobpy

Shader code
shader_type spatial;
render_mode world_vertex_coords, cull_disabled;

uniform sampler2D screen : hint_screen_texture, filter_linear_mipmap_anisotropic, repeat_disable;
group_uniforms colours;
uniform vec3 surfacecolour : source_color;
uniform vec3 volumecolour : source_color;

group_uniforms material;
uniform float AirIOR = 1.0;
uniform float IOR = 1.33;

group_uniforms textures;
uniform vec2 sampler1speed = vec2(0.02, 0.0);
uniform vec2 sampler2speed = vec2(0.0, 0.02);
uniform float samplermix : hint_range(0.0, 1.0, 0.1) = 0.5;
uniform vec2 samplerscale = vec2(0.1);
uniform sampler2D normal1tex : filter_linear_mipmap_anisotropic, hint_normal;
uniform sampler2D normal2tex : filter_linear_mipmap_anisotropic, hint_normal;
uniform float normalstrength : hint_range(0.0, 5.0, 0.01) = 1.0;
uniform sampler2D height1tex : filter_linear_mipmap_anisotropic;
uniform sampler2D height2tex : filter_linear_mipmap_anisotropic;
uniform float heightstrength : hint_range(0.0, 5.0, 0.01) = 0.12;
uniform sampler2D edge1tex : filter_linear_mipmap_anisotropic;
uniform sampler2D edge2tex : filter_linear_mipmap_anisotropic;

varying vec2 position;
varying vec3 wposition;

group_uniforms refraction;

uniform float refrationamount : hint_range(0.0, 1.0, 0.01);
uniform bool fog_underwater;

group_uniforms edge;

uniform float edge_size : hint_range(0.01, 0.5, 0.01) = 0.1;
uniform bool foam_or_fade = false;

uniform sampler2D DEPTH_TEXTURE : hint_depth_texture, filter_linear_mipmap, repeat_disable;

group_uniforms screen_space_reflection;

uniform float far_clip = 50.0;
uniform int steps : hint_range(64, 1024, 16) = 512;
uniform float ssr_screen_fade : hint_range(0.01, 0.5, 0.01) = 0.05;

float schlickfresnel(float ior1, float ior2, vec3 view, vec3 norm) {
	float incident = dot(view, norm);
	float reflectance = pow(((ior2 - ior1)/(ior2 + ior1)), 2.0);
	float fresnelincident = reflectance + (1.0 - reflectance) * pow(1.0 - cos(incident), 5.0);
	return(fresnelincident / incident);
}

void vertex() {
	position = VERTEX.xz;
	UV = VERTEX.xz * samplerscale + (sampler1speed * TIME);
	UV2 = VERTEX.xz * samplerscale + (sampler2speed * TIME);
	float height = mix(texture(height1tex, UV),texture(height2tex, UV2),samplermix).x;
	VERTEX.y += (height - 0.5) * heightstrength;
	wposition = VERTEX;
	// Called for every vertex the material is visible on.
}

float snells_window(vec3 normal, vec3 view, float ior) {
	float cos_theta = dot(normal, view);
	return step(sqrt(1.0 - cos_theta * cos_theta) * ior, 1.0);
}

float linear_depth(float nonlinear_depth, mat4 inv_projection_matrix) {
	return 1.0 / (nonlinear_depth * inv_projection_matrix[2].w + inv_projection_matrix[3].w);
}

float nonlinear_depth(float linear_depth, mat4 inv_projection_matrix) {
	return (1.0 / linear_depth - inv_projection_matrix[3].w) / inv_projection_matrix[2].w;
}

vec2 view2uv(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;
}

float remap(float x, float min1, float max1, float min2, float max2) {
	return ((x - min1) / (max1 - min1) + min2) * (max2 - min2);
}
float remap1(float x, float min1, float max1) {
	return (x - min1) / (max1 - min1);
}

float edge_fade(vec2 uv, float size) {
	float x1 = clamp(remap1(uv.x, 0.0, size), 0.0, 1.0);
	float x2 = clamp(remap1(uv.x, 1.0, 1.0 - size), 0.0, 1.0);
	float y1 = clamp(remap1(uv.y, 0.0, size), 0.0, 1.0);
	float y2 = clamp(remap1(uv.y, 1.0, 1.0 - size), 0.0, 1.0);
	return x1*x2*y1*y2;
}

void fragment() {

	vec3 onorm = NORMAL;

	vec2 normmap = mix(texture(normal1tex, UV),texture(normal2tex, UV2),samplermix).xy;
	NORMAL += TANGENT * (normmap.x - 0.5) * normalstrength;
	NORMAL += BINORMAL * (normmap.y - 0.5) * normalstrength;

	vec3 wnorm = (vec4(NORMAL, 0.0) * VIEW_MATRIX).xyz;
	vec3 wview = (vec4(VIEW, 0.0) * VIEW_MATRIX).xyz;
	if (FRONT_FACING) {

		ROUGHNESS = 0.0;
		METALLIC = 1.0;
		SPECULAR = 0.0;

		float fres = schlickfresnel(AirIOR, IOR, VIEW, NORMAL);
		ALBEDO = surfacecolour * fres;
		
		// REFRACTION
		
		float lineardepth = linear_depth(texture(DEPTH_TEXTURE, SCREEN_UV).r, INV_PROJECTION_MATRIX);
		float selfdepth = -VERTEX.z;
		float depth_diff = lineardepth - selfdepth;
		vec3 tanx = BINORMAL * (normmap.x - 0.5) * normalstrength;
		vec3 tany = TANGENT * (normmap.y - 0.5) * normalstrength;
		vec2 refracted_uv = SCREEN_UV + (tanx + tany).xy * refrationamount * depth_diff / lineardepth;
		float newdepth = linear_depth(texture(DEPTH_TEXTURE, refracted_uv).r, INV_PROJECTION_MATRIX);
		//float selfdepth = 1.0/(1.0 + 2.0 * distance(wposition, CAMERA_POSITION_WORLD));
		vec3 newvolcolour = mix(volumecolour, vec3(1.0), clamp(1.0 / (depth_diff * 1.0), 0.0, 1.0));
		EMISSION = newvolcolour * texture(screen, refracted_uv).rgb;

		if (newdepth < selfdepth) {
			EMISSION = newvolcolour * texture(screen, SCREEN_UV).rgb;
		}
		
		// SSR
		
		vec3 reflected = -reflect(VIEW, NORMAL);
		vec3 pos = VERTEX;
		int curstep = 0;
		bool finished = false;
		vec2 uv;
		float currentdepth;
		while (curstep < steps) {
			float step_scale = float(curstep + 1) / float(steps);
			float step_dist = step_scale * step_scale * far_clip;
			pos += reflected * step_dist;
			curstep += 1;
			currentdepth = -pos.z;
			uv = view2uv(pos, PROJECTION_MATRIX);
			if (!(uv.x < 1.0 && uv.y < 1.0 && uv.x > 0.0 && uv.y > 0.0)) {
				break;
			}
			float testdepth = linear_depth(texture(DEPTH_TEXTURE, uv).r, INV_PROJECTION_MATRIX);
			if (testdepth < currentdepth) {
				finished = true;
				break;
			}
		}
		if (finished && currentdepth < far_clip * 0.99) {
			ALBEDO *= 1.0 - edge_fade(uv, ssr_screen_fade);
			METALLIC *= 1.0 - edge_fade(uv, ssr_screen_fade);
			EMISSION += texture(screen, uv).xyz * schlickfresnel(1.0, 1.33, VIEW, NORMAL) * edge_fade(uv, ssr_screen_fade);
		}
		
		// EDGE EFFECT
		
		float distfromedge = depth_diff * dot(normalize(NORMAL), normalize(-VERTEX)) / VIEW.z;
		if (distfromedge < edge_size) {
			distfromedge /= edge_size;
			if (foam_or_fade) {
				ALPHA = distfromedge;
			} else {
				float edgetex = mix(texture(edge1tex, UV).r, texture(edge2tex, UV2).r, samplermix);
				if (edgetex > distfromedge) {
					ALBEDO = vec3(1.0);
					ROUGHNESS = 1.0;
					METALLIC = 1.0;
					EMISSION = vec3(0.0);
					NORMAL = onorm;
				}
			}
		}

	} else {
		
		// SNELLS WINDOW
		float window = snells_window(wnorm, wview, IOR);

		if (window > 0.5) {
			ROUGHNESS = 1.0;
			METALLIC = 1.0;
			ALBEDO = vec3(0.0);
			SPECULAR = 0.0;
			float linear_depth = 1.0 / (texture(DEPTH_TEXTURE, SCREEN_UV).r * INV_PROJECTION_MATRIX[2].w + INV_PROJECTION_MATRIX[3].w);
			float selfdepth = 1.0 / (FRAGCOORD.z * INV_PROJECTION_MATRIX[2].w + INV_PROJECTION_MATRIX[3].w);
			float depth_diff = linear_depth - selfdepth;
			vec3 tanx = BINORMAL * (normmap.x - 0.5) * normalstrength;
			vec3 tany = TANGENT * (normmap.y - 0.5) * normalstrength;
			float newdepth = 1.0 / (texture(DEPTH_TEXTURE, SCREEN_UV + (tanx + tany).xy * refrationamount).r * INV_PROJECTION_MATRIX[2].w + INV_PROJECTION_MATRIX[3].w);
			//float selfdepth = 1.0/(1.0 + 2.0 * distance(wposition, CAMERA_POSITION_WORLD));
			vec3 newvolcolour = mix(volumecolour, vec3(1.0), clamp(1.0 / (selfdepth * 1.0), 0.0, 1.0));
			if (!fog_underwater) {
				newvolcolour = vec3(1.0);
			}
			EMISSION = newvolcolour * texture(screen, SCREEN_UV + (tanx + tany).xy * refrationamount).rgb;
		} else {
			ALBEDO = surfacecolour;
			ROUGHNESS = 0.0;
			METALLIC = 1.0;
			
			// SSR
			
			vec3 reflected = -reflect(VIEW, NORMAL);
			vec3 pos = VERTEX;
			int curstep = 0;
			bool finished = false;
			vec2 uv;
			float currentdepth;
			while (curstep < steps) {
				float step_scale = float(curstep + 1) / float(steps);
				float step_dist = step_scale * step_scale * far_clip;
				pos += reflected * step_dist;
				curstep += 1;
				currentdepth = -pos.z;
				uv = view2uv(pos, PROJECTION_MATRIX);
				if (!(uv.x < 1.0 && uv.y < 1.0 && uv.x > 0.0 && uv.y > 0.0)) {
					break;
				}
				float testdepth = linear_depth(texture(DEPTH_TEXTURE, uv).r, INV_PROJECTION_MATRIX);
				if (testdepth < currentdepth) {
					finished = true;
					break;
				}
			}
			if (finished && currentdepth < far_clip * 0.99) {
				ALBEDO *= 1.0 - edge_fade(uv, ssr_screen_fade);
				METALLIC *= 1.0 - edge_fade(uv, ssr_screen_fade);
				EMISSION += texture(screen, uv).xyz * schlickfresnel(1.0, 1.33, VIEW, NORMAL) * edge_fade(uv, ssr_screen_fade);
			}
		}

	}
}
Tags
reflection, refraction, SSR, water
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 smallcableboi

Sky++ but world-space

Related shaders

Transparent Water Shader supporting SSR and Refraction

[2D]Water reflection and distortion simulation shader ver1.2

Water Shader for Realistic Look

guest

6 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
LGWV
LGWV
4 months ago

Great looking shader, but I had some issues testing it out on my end. Are you able to share an example project with this shader by chance? Thanks!

Florian
Florian
4 months ago
Reply to  LGWV

I have applied it on a planemesh, it seems to work pretty well.
What are your issues ?

Florian
Florian
4 months ago

The quality of this shader is astonishing !
It blow my mind ! Well done !

Jake
Jake
3 months ago

How did you achieve the fog affect in screenshot 3? Could you provide a demo project for this shader? Thanks!

Cekour
2 months ago

Awesome, but fog under water doesn’t work in 4.4.1
Or maybe you are using a fog volume for the under water screenshot?
If so, what is the fog underwater setting for?
Because it doesn’t seems to change anything on my end.

Last edited 2 months ago by Cekour