A fully interactive water system designed for 2D and 2.5D games (works for 3D too, but some features are quite redundant).
- GPU-driven ambient surface waves using Unity Shader Graph.
- GPU-driven and real-time contact ripples using Unity Shader.
- Fully configurable caustics and distortion using Unity Shader Graph.
- Real-time planar reflections.
- Fully compatible with the Unity 2D Light System.
- Accurate depth clipping with both sprites and meshes.
There are 2 demo scenes included in the project, Demo_WaterOnly and Demo_WithLighting. This video and this live demo are from Demo_WithLighting.
The water system creates 2 meshes:
- A "Front mesh" on the XY plane that is meant to be interpreted as the cross-section of a water body. The front mesh applies caustics and distortions to sprites behind it.
- A "Top mesh" on the XZ plane that is meant to be interpreted as the water surface. The top mesh has waves, ripples, and reflections.
In the project files, you can find:
- The
CRT_AmbientWaveshader graph that renders ambient waves to theAmbientWaverender texture. - The
CRT_RippleSimulationshader that renders all ripple interactions to theRippleSimulationrender texture. This texture is run on demand in theFixedUpdateloop of the InteractiveWater script. - The SimplePlanarReflection.cs script that renders the reflections of reflectable sprites to the
PlanarReflectionrender texture. - The
FrontMeshshader graph that uses theAmbientWaveandRippleSimulationtextures for vertex displacement, and applies caustics and distortions to sprites behind it based on sorting layer order. - The
TopMeshshader graph that uses theAmbientWaveandRippleSimulationtextures for vertex displacement, and thePlanarReflectiontexture for reflections.
The system needs the following configuration requirements that are not visible within the WaterSystem folder but still exist somewhere in the project:
-
2 additional layers:
WaterSystem_Water(you can use the defaultWaterlayer) andWaterSystem_Reflections. Any GameObject with an attached SpriteRenderer component and layer set toWaterSystem_Reflectionswill appear in the reflection texture. This also works for Light2D lighting, although I prefer a single, dark color for my reflections. TheWaterSystem_Waterlayer is only used for creating surface ripples from mouse input (which requires raycasting to a specific layer).

-
2 additional sorting layers:
WaterTopMeshandWaterFrontMesh.WaterTopMeshis used to mark where the Camera Sorting Layer Texture should stop.WaterFrontMeshcan be replaced by any sorting layer that comes afterWaterTopMesh. The other layers are for my own demo scene setup and are not required. Please note that theDefaultsorting layer should always come before the required sorting layers.

-
A custom Renderer2D with
Camera Sorting Layer Textureenabled and set toWaterTopMesh. This is necessary for getting everything behind the front mesh to apply caustics and distortions on top of them.

-
You also need a Global Volume for the 2D lights to show up. I forget this quite often.
- Made in Shader Graph and outputs to a Custom Render Texture.
- Contains 3 layers of sinusoidal waves, with multiple configuration parameters.

- "That's a blatant lie! I saw 3 CustomFunction nodes!", I only use it to shorten this whole thing:

- Made with a shader, the ripples need to differentiate between when a contact happens and normal simulation, which can be interpreted as different shader passes, which Shader Graph doesn't support, hence the use of a good ol' shader. I'm planning on converting it to Shader Graph somehow, so for the visual learners out there, come back in a few months, or years...
- Contact ripples can be created anywhere, with adjustable strength and initial direction (up/down).
- The initial shape of the ripple is currently set to a very small square, but it can be changed in code.

- Captures reflections with an additional camera, outputs to a Render Texture, with configurable resolution.
- The reflection texture is simply applied on top of the top mesh, which has yet to have correct surface normals after all the waves and ripples, making it a bit odd and static.

- In Unity, all sprites are rendered on the Transparent queue, which means they aren't written to the depth buffer.
- The top mesh exists on the XZ plane and will clip into some sprites at some point. But since its base shader is Sprite Lit (for Light 2D compatibility), it doesn't have any depth data to clip into the sprites. Instead, the render order is based on the sorting order layer, meaning either a whole sprite is on top of or behind the water, no matter what the z-coordinate is.
- I initially solved this by attaching 2 materials to a sprite renderer, one unlit to make a "cutout" of the sprite on the water surface, then one lit to render the actual sprite.
- Then I realized while writing this that I could just make the sprite lit shader write to the depth buffer...

- Add the correct surface normals to fix static reflections.
- Caustics and Distortions are visually squashed and influenced by the waves. I'm gonna do something about that.
- The planar reflection is set to generate reflections based on a defined plane and normal, which contributes to the static feeling of the reflections, which needs improvement.

