Pixel Art Water
This is a port of the water shader jess::codes created in Unity for her pixel art game.
Since Jess has her world generated automatically, I also adjusted the shader a bit. Instead of a global heightmap, I use the color channels of the texture:
- Red = outline
- Green = foam
- Blue = depth/transparency
To use the shader together with a tilemap as in the title image, the tilemap must be packed into a subviewport. This must then be set as the ViewportTexture of a Sprite2D node outside the SubViewport. The shader is assigned to the Sprite2D node. I have attached my tilemap as an example, but the foam and depth areas are relatively small in this case.
In addition, a caustic texture, a texture for the caustic highlights, and one (or multiple) noise textures are required.
Uniforms
- aspectRatio – This is used to scale the textures. Can be adjusted via script and set_shader_parameter(“aspectRatio”, value)
- pixelization – For scaling the pixels
- waterDepthGradient – A color gradient for the water depth. On the left is the color for deep water, on the right for shallow water.
- causticColor – Color of the caustic effect
- causticHighlightColor – The color of the caustic highlights
- causticTexture – Caustic Texture (see attached images)
- causticHighlightTexture – Caustic highlight texture (see attached images)
- causticNoiseTexture – For the movement of the caustic effect
- causticFadeNoiseTexture – Controls the visibility of the caustic effect
- causticScale – Scales the caustic textures
- causticSpeed – The speed at which the caustic effect moves
- causticMovementAmount – How much the caustic effect moves
- causticFaderMultiplier – How strongly the causticFadeNoiseTexture is affected
- specularColor – The color of the specular highlights
- specularNoiseTexture – Defines where specular highlights are displayed
- specularMovementLeftNoiseTexture – For the movement of the specular effect
- specularMovementRightNoiseTexture – For the movement of the specular effect
- specularThreshold – How many specular highlights are visible
- specularSpeed – The movement speed of the specular highlights
- specularScale – The size of the specular highlights
- foamColor – The color of the foam on the shore
- foamTexture – Defines where foam is displayed
- foamIntensity – How strong the foam is visible
- foamScale – The size of the foam
- outlineColor – The color of the outline on the shore
- generalTransparency – Controls the intensity with which the blue value affects the transparency of the water
Shader code
shader_type canvas_item;
uniform float aspectRatio = 1.0f;
uniform float pixelization = 2048.0f;
uniform sampler2D waterDepthGradient : hint_default_black;
uniform vec4 causticColor = vec4(0.455f, 0.773f, 0.765f, 1.0f);
uniform vec4 causticHighlightColor = vec4(0.741f, 0.894f, 0.898f, 1.0f);
uniform sampler2D causticTexture : hint_default_white, repeat_enable;
uniform sampler2D causticHighlightTexture : hint_default_white, repeat_enable;
uniform sampler2D causticNoiseTexture : hint_default_white, repeat_enable;
uniform sampler2D causticFadeNoiseTexture : hint_default_white, repeat_enable;
uniform float causticScale = 12.0f;
uniform float causticSpeed = 0.005f;
uniform float causticMovementAmount = 0.15f;
uniform float causticFaderMultiplier = 1.45f;
uniform vec4 specularColor = vec4(1.0f, 1.0f, 1.0f, 1.0f);
uniform sampler2D specularNoiseTexture : hint_default_white, repeat_enable;
uniform sampler2D specularMovementLeftNoiseTexture : hint_default_white, repeat_enable;
uniform sampler2D specularMovementRightNoiseTexture : hint_default_white, repeat_enable;
uniform float specularThreshold = 0.35f;
uniform float specularSpeed = 0.025f;
uniform float specularScale = 15.0f;
uniform vec4 foamColor = vec4(1.0f, 1.0f, 1.0f, 1.0f);
uniform sampler2D foamTexture : hint_default_white, repeat_enable;
uniform float foamIntensity = 0.2f;
uniform float foamScale = 15.0f;
uniform vec4 outlineColor = vec4(0.675f, 0.86f, 1.0f, 1.0f);
uniform float generalTransparency = 1.0f;
// ------------------------------------------------------------------------------------
// Helper functions
// Blends two vec2's by subtracting them. Compare this to Photoshop blend mode "Subtract".
// Source:
// https://docs.unity3d.com/Packages/com.unity.shadergraph@6.9/manual/Blend-Node.html
vec2 blendSubtract_vec2(vec2 base, vec2 blend, float opacity)
{
vec2 result = base - blend;
return mix(base, result, opacity);
}
// Blends two floats by subtracting them. Compare this to Photoshop blend mode "Subtract".
// Source:
// https://docs.unity3d.com/Packages/com.unity.shadergraph@6.9/manual/Blend-Node.html
float blendSubtract_float(float base, float blend, float opacity)
{
float result = base - blend;
return mix(base, result, opacity);
}
// Blends two vec2's by overlaying them. Compare this to Photoshop blend mode "Overlay".
// Source:
// https://docs.unity3d.com/Packages/com.unity.shadergraph@6.9/manual/Blend-Node.html
float blendOverlay_float(float base, float blend, float opacity)
{
float result1 = 1.0f - 2.0f * (1.0f - base) * (1.0f - blend);
float result2 = 2.0f * base * blend;
float zeroOrOne = step(0.5f, base);
float res = result2 * zeroOrOne + (1.0 - zeroOrOne) * result1;
return mix(base, res, opacity);
}
// Pixelizes the given coordinate.
vec2 pixelizeCoordinates(vec2 coordinates)
{
return floor(coordinates * pixelization) / pixelization;
}
// Applies the aspect ratio to the coordinates.
vec2 applyAspectRatio(vec2 coordinates)
{
return vec2(coordinates.x, coordinates.y * aspectRatio);
}
// ------------------------------------------------------------------------------------
// Shader layers
vec4 caustics(vec2 pixelizedCoordinates)
{
vec4 causticNoise = texture(causticNoiseTexture, TIME * causticSpeed + pixelizedCoordinates);
vec2 noiseCoordinates = blendSubtract_vec2(pixelizedCoordinates * causticScale, causticNoise.rg, causticMovementAmount);
vec4 causticHighlight = texture(causticHighlightTexture, noiseCoordinates) * causticHighlightColor;
vec4 caustic = texture(causticTexture, noiseCoordinates) * causticColor;
vec4 interpolatedCaustics = mix(caustic, causticHighlight, causticHighlight.a);
float fadeNoise = texture(causticFadeNoiseTexture, noiseCoordinates).r * causticFaderMultiplier;
return vec4(interpolatedCaustics.r, interpolatedCaustics.g, interpolatedCaustics.b, clamp(interpolatedCaustics.a - fadeNoise, 0.0, 1.0));
}
vec4 specular(vec2 pixelizedCoordinates)
{
vec2 scaledCoordinates = pixelizedCoordinates * specularScale;
float specularNoise = texture(specularNoiseTexture, scaledCoordinates).r;
float leftScrollingNoise = texture(specularMovementLeftNoiseTexture, scaledCoordinates + vec2(TIME * specularSpeed, 0.0f)).r;
float rightScrollingNoise = texture(specularMovementRightNoiseTexture, scaledCoordinates + vec2(TIME * specularSpeed * -1.0f, 0.0f)).r;
return step(specularThreshold, blendSubtract_float(blendOverlay_float(leftScrollingNoise, rightScrollingNoise, 1.0f), specularNoise, 1.0f)) * specularColor;
}
vec4 foam(vec2 pixelizedCoordinates, vec4 mainTexColor)
{
vec4 colorizedFoam = texture(foamTexture, pixelizedCoordinates * foamScale) * foamColor;
float intensity = clamp(mainTexColor.g * mainTexColor.a - foamIntensity, 0.0f, 1.0f);
return vec4(colorizedFoam.r, colorizedFoam.g, colorizedFoam.b, colorizedFoam.a * intensity);
}
// ------------------------------------------------------------------------------------
// Fragment Shader code
void fragment()
{
vec2 pixelizedCoordinates = pixelizeCoordinates(applyAspectRatio(UV));
vec4 mainTex = texture(TEXTURE, UV);
vec4 depthBasedWaterColor = texture(waterDepthGradient, vec2(1.0f - mainTex.b, 1.0f));
vec4 finalCaustics = caustics(pixelizedCoordinates);
vec4 finalSpecular = specular(pixelizedCoordinates);
vec4 finalFoam = foam(pixelizedCoordinates, mainTex);
vec4 waterWithCausticLayer = mix(depthBasedWaterColor, finalCaustics, finalCaustics.a);
vec4 waterWithCausticAndSpecularLayer = mix(waterWithCausticLayer, finalSpecular, ceil(finalCaustics.a) * finalSpecular.a);
vec4 waterWithCausticAndSpecularAndFoamLayer = mix(waterWithCausticAndSpecularLayer, finalFoam, finalFoam.a);
float outline = mainTex.a * mainTex.r;
vec4 finalOutlineColor = outline * outlineColor;
vec4 finalRGBColor = mix(waterWithCausticAndSpecularAndFoamLayer, finalOutlineColor, outline);
COLOR = vec4(finalRGBColor.r, finalRGBColor.g, finalRGBColor.b, mainTex.b * generalTransparency);
}
How are the edge animations made? I want to make animations under [Terrain], but I don’t know how to do it.
You can setup animated tiles in the TileSet editor. First you need to select only the tiles that represent the first frame (= first column) under “Setup”. Then switch to “Select” tab, select the tiles and expand the “Animation” settings. There you can define the number of columns for the animation (= number of frames) and also define durations for each frame.
For this to work, you need to set up your tilemap so that the frames of an animation are next to each other.
Please post a video walkthrough or more detailed step-by-step instructions. This is really cool but very hard to implement. I’ve tried about six times now with varying results.
you can put water under the terrain layer.
Would you be able to upload an example project using this shader?
I managed to get it working after six attempts in Godot 4. Here is the project link. https://github.com/TajiDev/Pixel-Water-Shader
I don’t quite get how i would get this to running 🙁 can you explain where i get the images from? maybe a example scene would help
I managed to get it working after six attempts in Godot 4. Here is the project link. https://github.com/TajiDev/Pixel-Water-Shader
This is really cool but I cannot find anywhere online on how to actually use this. Even researching other walkthrough tutorials on Youtube I cannot manage to get this working with the provided assets.
Working Godot 4 project for reference. Great overall code. It’s pretty hard to implement from current directions. https://github.com/TajiDev/Pixel-Water-Shader