Lowpoly Water with sspr

Use this gdscript,you should modify the shader path:

@tool
extends MeshInstance3D

@export var grid_size = 16.0:
	set(value):
		if grid_size != value:
			grid_size = value
			if Engine.is_editor_hint():
				call_deferred("_update_mesh")

@export var subdivision = 8:
	set(value):
		if subdivision != value:
			subdivision = value
			if Engine.is_editor_hint():
				call_deferred("_update_mesh")

@export var wave_0 : Vector4 = Vector4(0.3, 2.0, 1.0, 0.0)
@export var wave_1 : Vector4 = Vector4(0.2, 1.5, 0.8, 2.5)
@export var wave_2 : Vector4 = Vector4(0.15, 1.0, 1.2, 5.0)
@export var wave_3 : Vector4 = Vector4(0.1, 0.8, 0.6, 8.0)

@export var noise_scale : float = 1.0
@export var noise_strength : float = 0.3

@export var albedo : Color = Color(0.76, 0.94, 0.93, 0.8)
@export var metallic : float = 0.1
@export var roughness : float = 0.3
@export var water_level : float = 0.5
@export var scale_value : float = 1.0

@export_group("Edge Foam")
@export var edge_threshold : float = 0.1
@export var edge_softness : float = 0.5
@export var foam_color : Color = Color(1.0, 1.0, 1.0, 1.0)

var material : ShaderMaterial

func _enter_tree():
	if Engine.is_editor_hint():
		create_lowpoly_water()
		setup_material()

func _exit_tree():
	if Engine.is_editor_hint():
		mesh = null

func _ready():
	create_lowpoly_water()
	setup_material()

func _update_mesh():
	create_lowpoly_water()
	setup_material()

func create_lowpoly_water():
	var st = SurfaceTool.new()
	st.begin(Mesh.PRIMITIVE_TRIANGLES)

	var offset = grid_size / 2.0
	var step = grid_size / float(subdivision)

	for z in range(subdivision):
		for x in range(subdivision):
			var x0 = x * step - offset
			var z0 = z * step - offset
			var x1 = (x + 1) * step - offset
			var z1 = (z + 1) * step - offset

			var v0 = Vector3(x0, 0.0, z0)
			var v1 = Vector3(x1, 0.0, z0)
			var v2 = Vector3(x0, 0.0, z1)
			var v3 = Vector3(x1, 0.0, z1)

			var normal = Vector3.UP
			var uv0 = Vector2(float(x) / subdivision, float(z) / subdivision)
			var uv1 = Vector2(float(x + 1) / subdivision, float(z) / subdivision)
			var uv2 = Vector2(float(x) / subdivision, float(z + 1) / subdivision)
			var uv3 = Vector2(float(x + 1) / subdivision, float(z + 1) / subdivision)

			st.set_normal(normal)
			st.set_uv(uv0)
			st.add_vertex(v0)
			st.set_uv(uv2)
			st.add_vertex(v2)
			st.set_uv(uv1)
			st.add_vertex(v1)

			st.set_normal(normal)
			st.set_uv(uv2)
			st.add_vertex(v2)
			st.set_uv(uv3)
			st.add_vertex(v3)
			st.set_uv(uv1)
			st.add_vertex(v1)

	st.generate_normals()
	mesh = st.commit()

func setup_material():
	material = ShaderMaterial.new()
	var shader = load("res://#Template/[Scenes]/DefaultScene/new_shader.gdshader")
	material.shader = shader
	material.render_priority = 1
	update_shader_params()
	mesh.surface_set_material(0, material)

func update_shader_params():
	if not material:
		return

	material.set_shader_parameter("wave_0", wave_0)
	material.set_shader_parameter("wave_1", wave_1)
	material.set_shader_parameter("wave_2", wave_2)
	material.set_shader_parameter("wave_3", wave_3)
	material.set_shader_parameter("noise_scale", noise_scale)
	material.set_shader_parameter("noise_strength", noise_strength)
	material.set_shader_parameter("albedo", albedo)
	material.set_shader_parameter("metallic", metallic)
	material.set_shader_parameter("roughness", roughness)
	material.set_shader_parameter("water_level", water_level)
	material.set_shader_parameter("scale", scale_value)
	material.set_shader_parameter("edge_threshold", edge_threshold)
	material.set_shader_parameter("edge_softness", edge_softness)
	material.set_shader_parameter("foam_color", foam_color)

func _process(_delta):
	if material:
		update_shader_params()
Shader code
shader_type spatial;
render_mode cull_disabled, depth_prepass_alpha, blend_mix;

// --- Depth & Screen textures for SSR and edge detection ---
uniform sampler2D depth_texture : hint_depth_texture, filter_nearest, repeat_disable;
uniform sampler2D screen_texture : hint_screen_texture, filter_nearest, repeat_disable;

const float z_near = 0.05;
const float z_far = 4000.0;

// --- Gerstner Wave: (amplitude, frequency, speed, phase) ---
uniform vec4 wave_0 = vec4(0.3, 2.0, 1.0, 0.0);
uniform vec4 wave_1 = vec4(0.2, 1.5, 0.8, 2.5);
uniform vec4 wave_2 = vec4(0.15, 1.0, 1.2, 5.0);
uniform vec4 wave_3 = vec4(0.1, 0.8, 0.6, 8.0);

// --- Noise control ---
uniform float noise_scale : hint_range(0.0, 10.0) = 1.0;
uniform float noise_strength : hint_range(0.0, 1.0) = 0.3;

// --- PBR ---
uniform vec4 albedo : source_color = vec4(0.76, 0.94, 0.93, 0.8);
uniform float metallic : hint_range(0.0, 1.0) = 0.1;
uniform float roughness : hint_range(0.0, 1.0) = 0.3;
uniform float specular : hint_range(0.0, 1.0, 0.01) = 0.5;

// --- Water control ---
uniform float water_level : hint_range(-5.0, 5.0, 0.01) = 0.0;
uniform float scale : hint_range(0.1, 100.0) = 1.0;

// --- Edge foam ---
uniform float edge_threshold : hint_range(0.0, 2.0, 0.01) = 0.1;
uniform float edge_softness : hint_range(0.0, 1.0) = 0.5;
uniform vec4 foam_color : source_color = vec4(1.0, 1.0, 1.0, 1.0);

// --- SSR settings ---
group_uniforms SSRSetting;
uniform int ssr_max_travel = 24;
uniform float depth_tolerance = 0.2;
uniform float ssr_fresnel_power : hint_range(0.1, 10.0) = 3.0;

varying vec3 v_view_pos;
varying vec3 v_world_pos;

// ============================================================
// Noise functions
// ============================================================
float hash(vec2 p) {
	return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}

float noise(vec2 p) {
	vec2 i = floor(p);
	vec2 f = fract(p);
	f = f * f * (3.0 - 2.0 * f);
	float a = hash(i);
	float b = hash(i + vec2(1.0, 0.0));
	float c = hash(i + vec2(0.0, 1.0));
	float d = hash(i + vec2(1.0, 1.0));
	return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}

float fbm(vec2 p) {
	float value = 0.0;
	float amplitude = 0.5;
	for (int i = 0; i < 3; i++) {
		value += amplitude * noise(p);
		p *= 2.0;
		amplitude *= 0.5;
	}
	return value;
}

// ============================================================
// Gerstner wave
// ============================================================
vec3 calculate_wave(vec3 position_ws, float amplitude, float frequency, float speed, float phase_offset) {
	float phase = TIME * speed + phase_offset;
	float wave_input = position_ws.x * 0.5 + position_ws.z * 0.3;
	float y = amplitude * sin(wave_input * frequency + phase);
	return vec3(0.0, y, 0.0);
}

// ============================================================
// SSR helper functions
// ============================================================
vec3 uv_to_view(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 view_to_uv(vec3 position_view_space, mat4 proj_m, out float w) {
	vec4 position_clip_space = proj_m * vec4(position_view_space.xyz, 1.0);
	vec3 position_ndc = position_clip_space.xyz / position_clip_space.w;
	w = position_clip_space.w;
	return vec3(position_ndc.xy * 0.5 + 0.5, position_ndc.z);
}

vec4 get_depth_tested_color(vec4 pos, vec4 advance, vec2 viewport_size, mat4 inv_mat) {
	vec2 pixel_size = 1.0 / viewport_size;
	float z_from = pos.z / pos.w;
	float z_to = z_from;
	vec2 uv;
	float depth;
	for (int i = 0; i < ssr_max_travel; i++) {
		pos += advance;
		uv = (pos.xy - 0.5) * pixel_size;
		depth = uv_to_view(uv, texture(depth_texture, uv).r, inv_mat).z;

		z_from = z_to;
		z_to = pos.z / pos.w;

		if (depth > z_to && depth <= max(z_to, z_from) + depth_tolerance && -depth < z_far * 1.0) {
			if (any(bvec4(lessThan(pos.xy, vec2(1.0, 1.0)), greaterThan(pos.xy, viewport_size))) == false) {
				return vec4(texture(screen_texture, (pos.xy - 0.5) * pixel_size).rgb, 1.0);
			}
		}
	}
	return vec4(0.0);
}

// ============================================================
// Vertex: Gerstner wave displacement
// ============================================================
void vertex() {
	vec3 pos = VERTEX * vec3(scale, 1.0, scale);
	vec3 raw = VERTEX;

	vec3 offset = vec3(0.0);
	offset += calculate_wave(raw, wave_0.x, wave_0.y, wave_0.z, wave_0.w);
	offset += calculate_wave(raw, wave_1.x, wave_1.y, wave_1.z, wave_1.w);
	offset += calculate_wave(raw, wave_2.x, wave_2.y, wave_2.z, wave_2.w);
	offset += calculate_wave(raw, wave_3.x, wave_3.y, wave_3.z, wave_3.w);

	float noise_val = fbm(raw.xz * noise_scale + TIME * 0.5);
	offset.y += noise_val * noise_strength * (wave_0.x + wave_1.x);

	VERTEX = pos + offset + vec3(0.0, water_level, 0.0);
	v_view_pos = (MODELVIEW_MATRIX * vec4(VERTEX, 1.0)).xyz;
	v_world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
}

// ============================================================
// Fragment: flat normals + SSR + edge foam
// ============================================================
void fragment() {
	// --- Flat normal for low-poly look ---
	vec3 ddx = dFdx(v_view_pos);
	vec3 ddy = dFdy(v_view_pos);
	vec3 flat_normal_vs = normalize(cross(ddx, ddy));

	if (dot(flat_normal_vs, NORMAL) < 0.0) {
		flat_normal_vs = -flat_normal_vs;
	}

	// --- Edge foam via depth comparison ---
	float scene_depth = texture(depth_texture, SCREEN_UV).r;
	vec3 scene_pos = uv_to_view(SCREEN_UV, scene_depth, INV_PROJECTION_MATRIX);

	float water_depth = FRAGCOORD.z;
	vec3 water_pos = uv_to_view(SCREEN_UV, water_depth, INV_PROJECTION_MATRIX);

	float depth_diff = water_pos.z - scene_pos.z;
	float foam = 1.0 - smoothstep(0.0, edge_threshold, depth_diff);
	foam = pow(foam, edge_softness);

	// --- SSR ray marching ---
	vec2 pixel_size = 1.0 / VIEWPORT_SIZE;
	vec3 vertex = VERTEX;

	vec3 view_dir = normalize(vertex);
	vec3 ray_dir = normalize(reflect(view_dir, flat_normal_vs));

	float ray_len = (vertex.z + ray_dir.z * z_far) > -z_near ? (-z_near - vertex.z) / ray_dir.z : z_far;
	vec3 ray_end = vertex + ray_dir * ray_len;

	float w_begin;
	vec3 vp_line_begin = view_to_uv(vertex, PROJECTION_MATRIX, w_begin);
	float w_end;
	vec3 vp_line_end = view_to_uv(ray_end, PROJECTION_MATRIX, w_end);
	vec2 vp_line_dir = vp_line_end.xy - vp_line_begin.xy;

	w_begin = 1.0 / w_begin;
	w_end = 1.0 / w_end;

	float z_begin = vertex.z * w_begin;
	float z_end = ray_end.z * w_end;

	vec2 line_begin = vp_line_begin.xy / pixel_size;
	vec2 line_dir = vp_line_dir / pixel_size;
	float z_dir = z_end - z_begin;
	float w_dir = w_end - w_begin;

	float scale_max_x = min(1.0, 0.99 * (1.0 - vp_line_begin.x) / max(1e-5, vp_line_dir.x));
	float scale_max_y = min(1.0, 0.99 * (1.0 - vp_line_begin.y) / max(1e-5, vp_line_dir.y));
	float scale_min_x = min(1.0, 0.99 * vp_line_begin.x / max(1e-5, -vp_line_dir.x));
	float scale_min_y = min(1.0, 0.99 * vp_line_begin.y / max(1e-5, -vp_line_dir.y));
	float line_clip = min(scale_max_x, scale_max_y) * min(scale_min_x, scale_min_y);

	line_dir *= line_clip;
	z_dir *= line_clip;
	w_dir *= line_clip;

	float line_length = length(line_dir);

	float dist = length(CAMERA_POSITION_WORLD - v_world_pos);
	float linear_dist = clamp(dist / 200.0, 0.0, 1.0);
	float dynamic_step_size = mix(2.0, 8.0, linear_dist);

	float advance_angle_adj = 1.0 / max(abs(normalize(line_dir).x), abs(normalize(line_dir).y));
	vec4 line_advance = vec4(line_dir, z_dir, w_dir) * advance_angle_adj * (dynamic_step_size / line_length);
	vec4 init_pos = vec4(line_begin, z_begin, w_begin);

	// --- Get SSR color ---
	vec4 reflect_color;
	float reflect_amount;
	if (ray_dir.z > 0.0 || ray_len < 0.1) {
		reflect_color = vec4(0.0);
		reflect_amount = 0.0;
	} else {
		reflect_color = get_depth_tested_color(init_pos, line_advance, VIEWPORT_SIZE, INV_PROJECTION_MATRIX);
		reflect_amount = reflect_color.a * pow(1.0 - dot(flat_normal_vs, VIEW), ssr_fresnel_power);
	}

	// --- Final compositing ---
	vec3 base_color = mix(albedo.rgb, foam_color.rgb, foam * foam_color.a);
	vec3 final_color = mix(base_color, reflect_color.rgb, reflect_amount);

	NORMAL = flat_normal_vs;
	ALBEDO = final_color;
	ALPHA = albedo.a;
	METALLIC = metallic;
	ROUGHNESS = roughness;
	SPECULAR = specular;
	EMISSION = reflect_color.rgb * reflect_amount;
}
Live Preview
Tags
water
The shader code and all code snippets in this post are under MIT license and can be used freely. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

Related shaders

guest

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
DearFox
4 days ago

Well, apparently it doesn’t work properly in godot 4.6.

comment image