2D waterfall

Make a waterfall from a Sprite node. Resize the sprite to resize the waterfall. See it in action in the screenshots below.

There are several uniforms that can be set:

  • Refraction Map: Noise texture for the refraction effect. Use the built-in SimplexNoise.
  • Water Mask: Noise texture for the flow gaps. Use the built-in SimplexNoise.
  • Gap Stretch: Squeeze the texture to get the shape of the gaps as you like
  • Refraction Stretch: Squeeze the refraction texture to get the effect that you like
  • Refraction Strength: How intense you want the refraction effect to be
  • Water Tint: The color of the waterfall. Use alpha to lower intensity
  • Water Highlight: The color of gap edges. Use alpha to lower intensity
  • Speed: The speed of the flow
  • Flow Gaps: Size of the gaps
  • Highlight width: Size of the gap edges that will be colored with Water Highlight


Add a Sprite and set any texture to it (icon.png perhaps). Create the shader with the code below and set a new NoiseTexture with SimplexNoise to both Refraction Map and Water Mask. Check the Seamless parameter.

To make the waterfall behave the way you expect when rescaling and zooming you will have to add a bit of code to the Sprite and connect the item_rect_changed() signal. Create a new script for the Sprite and add the code below, don’t forget to connect the signal.

extends Sprite

func _process(delta):
	get_material().set_shader_param("zoom", get_viewport_transform().y.y)

#Connect the item_rect_changed() signal to this function
func _on_Waterfall_item_rect_changed():
	get_material().set_shader_param("scale", scale)

Don’t forget to add the tool keyword at the top of the script or the changes won’t be seen in the editor.

(The zoom effect is not consistent for all viewport settings when going from editor to the game. Perhaps someone know a better way to fix this)

That’s it! Play around with the different uniform values to get the waterfall look you want.

It’s not working

If it’s not working now, it is most likely because the scale and zoom parameters are not getting data from the script. Try reloading the scene and then resizing the sprite.

Shader code
Shader from Godot Shaders - the free shader library.

Feel free to improve and change this shader according to your needs
and consider sharing the modified result on godotshaders.com.

shader_type canvas_item;

uniform vec2 scale; // Used for sprite script. Don't edit this value in the inspector.
uniform float zoom; // Used for sprite script. Don't edit this value in the inspector.

uniform sampler2D refraction_map;
uniform sampler2D water_mask;

uniform vec2 gap_stretch = vec2(0.8, 0.05);
uniform vec2 refraction_stretch = vec2(2.0, 0.8);
uniform float refraction_strength : hint_range(0.0, 0.1) = 0.02;

uniform vec4 water_tint : hint_color = vec4(0.2, 0.6, 1.0, 0.1);
uniform vec4 water_highlight : hint_color = vec4(1.0, 1.0, 1.0, 0.3);
uniform float speed = 1.0;
uniform float flow_gaps : hint_range(0.0, 1.0) = 0.33;
uniform float highlight_width : hint_range(0.0, 0.3) = 0.02;

void fragment()
	// Get the two noise textures and make them move on the y axis. The gaps move twice as fast as the refraction, but you can tweak this by changing (speed * 0.5)
	vec2 refraction_offset = texture(refraction_map, vec2(UV.x, UV.y + -TIME * speed * 0.5) * scale * refraction_stretch).xy;
	vec2 gap_mask = texture(water_mask, vec2(UV.x, UV.y + -TIME * speed) * scale * gap_stretch).xy;
	// Set values between -0.5 and 0.5 (instead of 0 and 1). Otherwise the reflection will move whith increased refraction_strength
	refraction_offset -= 0.5; 
	// Get the screen texture and distort it
	vec4 refraction = texture(SCREEN_TEXTURE, SCREEN_UV - refraction_offset * refraction_strength * zoom);
	// Create holes and apply colors and textures //
	vec4 color = vec4(1.0);
	// Define what values will be the water highlight color (the gap border)
	float inner_edge = flow_gaps + highlight_width;
	// See if the pixel is within the edges range and use the water colors alpha to blend between showing color or refraction texture.
	if (gap_mask.x < inner_edge)
		color.rgb = mix(refraction.rgb, water_highlight.rgb, water_highlight.a);
		color.rgb = mix(refraction.rgb, water_tint.rgb, water_tint.a);
	// If the value is below the gap threshhold make it transparent (a gap)
	if (gap_mask.x < flow_gaps)
		color.a = 0.0;
	// Crate Edge Shape //
	// Set the shape for the top and bottom edges. Use water_mask as shape but with other values to flatten it out horizontally. 
	vec2 water_edge = texture(water_mask, vec2(UV.x, UV.y + -TIME * 0.1) * scale * vec2(0.15, 0.6)).xy;
	water_edge -= 0.5;
	// Use the same mask as for the gaps for left and right edge.
	vec2 vertical_edge_mask = gap_mask - 0.5;
	// Apply the new masks to the edges. This will make the wobble effect.
	color.a = mix(0.0, color.a, step(UV.x + vertical_edge_mask.x * 0.2, 0.92)); // Right edge
	color.a = mix(color.a, 0.0, step(UV.x - vertical_edge_mask.x * 0.2, 0.08)); // Left edge
	color.a = mix(0.0, color.a, step(UV.y + water_edge.y * 0.1, 0.95));  //Bottom edge
	color.a = mix(color.a, 0.0, step(UV.y - water_edge.y * 0.05, 0.05)); //Top edge
	COLOR = color;
2d, refraction, water, waterfall
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 pend00

VHS and CRT monitor effect

Brick/tiled wall

2D mirror effect


Newest Most Voted
Inline Feedbacks
View all comments
2 years ago

Thanks for making this! Unfortunately, after about an hour of trying, I was unable to make anything like the demo video. It is not trivial to pick the simplex noise parameters and the other parameters to make the effect work. Consider posting a runnable example, or try creating a new project from scratch to test your instructions and see how easy it is to make it work, then update the instructions.

2 years ago

This is great! I hijacked it a little bit to get a hologram effect. I changed the end of the shader code to this:

COLOR = texture(TEXTURE, UV);
COLOR.a = min(color.a, COLOR.a);
COLOR.rgb = mix(COLOR.rgb, color.rgb, 1.0 - 0.75 * length(color.rgb));
// COLOR = color;

And I set the settings to have a very low gap stretch x and high gap stretch y. Set to a negative speed. On noise, I chose a low period (18.6), high lacunarity (4) and low persistence (0.056). I would be happy to share some screenshots and/or video of the effect!

2 years ago

I really like the shader! Is there a way to pin the very top edge of the waterfall to make it look like it’s not moving?

2 years ago

Thank you for posting this! After following the instructions, all I see is a transparent blue rectangle with no movement in it.
Do you need to set the simplex parameters, or should they be left as default, to re-create your demo?
Would it be possible to post a working project?

Last edited 2 years ago by TheEasyWay
2 years ago

Why it is not working on Android?

1 year ago

Thanks a lot!
This is by far the best 2D waterfall shader I was able to find in the internet.

1 year ago

This Shader looks awesome in my game! thanks

1 year ago

That’s pretty amazing. 🙂

1 year ago


extends ColorRect

func _ready() -> void:
	get_material().set_shader_param("zoom", get_viewport_transform().y.y)

func _on_Waterfall_item_rect_changed() -> void:
	get_material().set_shader_param("scale", rect_scale)