choco_water.mp4
2.5D Dynamic Reflective Water system for Unity's Universal Rendering Pipeline (URP). Tested with both the URP 2D Renderer and Universal Forward Renderer. This package does not depend on compute shaders, and should run pretty much everywhere. Notably, it supports WebGL.
The code and shaders have been thoroughly commented, and every inspector field comes with a tooltip. Hopefully it will be helpful to people interested in implementation of these kinds of systems.
Inspired by ruccho's WaterRW and this article by Illham Effendi.
- This project is developed using Unity 2021.3.30f1, but should work with version 2021.3 and above in general.
- You'll also need to install the Universal Render Pipeline package.
- The easiest way is to just create your project with the 2D URP or 3D URP template.
- At the moment, ChocoWater's rendering requires the target platform to support single-channel float textures (also known as
R32F
).
ChocoWater is distributed as a git package. Use Unity's Package Manager and install using this repository's URL: https://github.com/chocola-mint/ChocoWater.git
.
Once installed, you'll want to add the ChocoWaterRenderFeature
to your Renderer 2D
asset, and change the Post Transparent Layer Mask
to contain only the layer where the water object is going to reside. For example, you may use the built-in "Water" layer.
Finally, add the prefab ChocoWater
by right-clicking the scene hierarchy and selecting "ChocoWater", and enter play mode to see the water rendered.
ChocoWater uses the shader Shader Graphs/Water Surface
. Properties in italics are automatically assigned, you do not need to assign them yourself.
Note that the built-in Prefab comes with a material using the same shader, but you're recommended to make your own copy and edit its properties as needed.
Property | Type | Description |
---|---|---|
DisplacementMap | Texture2D |
A 1D texture (Height=1) that describes the world-space surface displacement. Automatically created and updated by WaterVolume . |
ObjectSize | Vector2 |
The water's size in object space. Automatically assigned by WaterVolume . |
WaterColor | Color |
The base color of the water. |
DepthColor | Color |
A color used to darken the color of the water near the bottom. Uses multiplicative blending. |
SurfaceViewDepth | Float |
The desired size of the top-facing side of the water, in world space. |
Wave Depth Propagation Ratio | Float |
How much of the displacement should be applied to the edge of the water that's "closer" to the camera. Setting this to 1 will make waves look like ribbons. |
Surface Foam Depth | Float |
How "deep" the topmost white edge should go. |
Surface Foam Wave Scale | Float |
Used to scale the wavy pattern that makes up the surface foam. |
Surface Foam Wave Frequency | Float |
How quickly the surface foam's pattern should be played. |
Surface Foam Wave Depth | Float |
The maximum additional depth added to the surface foam line. |
Ripple Distortion Intensity | Float |
How harshly the water ripples should be distorted. |
Ripple Depth Propagation Ratio | Float |
A value of 1 causes ripples to cross the water surface completely, while a value of 0 stops it from propagating at all. Somewhere around 0.75 should be fine. |
Underwater Flow Frequency | Float |
How frequently should underwater pixels be distorted. |
Underwater Flow Intensity | Float |
How strong the underwater distortion should be, in screen space. |
Surface Flow Frequency | Float |
How frequently should surface pixels be distorted. |
Surface Flow Intensity | Float |
How strong the surface distortion should be in, in screen space. |
A water mesh is generated by the WaterVolume
component, subdivided according to the renderResolution
property. WaterVolume
also maintains a list of springs scattered evenly on the water's surface and runs physics simulation on Fixed Updates, and uploads their vertical displacements as a scalar (RFloat
) 1D texture to the GPU. This texture is sampled through a bilinear filter. Note that the number of springs isn't necessarily equal to the number of subdivisions on the water mesh.
To draw the water correctly, a Render Feature called ChocoWaterRenderFeature
is used. This render feature takes the screen output of the URP renderer and copies it to a global texture (by default, _CWScreenColor
), accessible by the Water Surface
shader graph. The Render Feature then draws "post-transparent" objects again, according to the specified layer mask (which should include the WaterVolume
objects).
- We can't use the
Camera Opaque Texture
provided by URP Cameras, because sprites are drawn as transparent meshes.
In the vertex shader, a displacement texture representing the list of springs on the surface is used to move the upper vertices into the correct positions.
In the fragment shader:
- Screen-Space Reflection is implemented by reading from the aforementioned
_CWScreenColor
texture and using the water surface as the axis of reflection. To hide artifacts caused by sampling out-of-screen pixels, the reflection fades out near the reflection limits, and pixels after that show the side view of the water instead. This behavior can also be overridden to maintain the water's view depth for as long as the surface may contain the screen reflection. - Refraction caused by the flowing water is faked using noise-based distortions sampling
_CWScreenColor
. - Wave foam is computed via distance from the water surface. For pixels where the wave displacement is above zero, a fake "splash ring" is also added with similar noise-based distortions as above.
As mentioned above, the WaterVolume
component maintains a chain of springs on the water surface. The simulation ticks every FixedUpdate
and its speed can be controlled via Time.timeScale
. Every spring is represented by a displacement and a velocity, and so WaterVolume
allocates two float buffers for the simulation.
For each physics step (WaterVolume.Step()
):
WaterVolume
moves every spring vertically according to its velocity.- It then updates every spring's velocity according to its displacement and velocity. This is what makes the spring feel like a spring.
- Afterwards, every spring spreads its velocity to its immediate neighbors, according to their height differences. This is what makes waves propagate along the entire water surface.
WaterVolume
provides a method WaterVolume.SurfaceImpact()
to interact with the water surface physically. If the water simulation is "predictable" in the sense that, you know ahead of time when and how objects will fall into the water, you can invoke this method manually. There are several simulation parameters that you can adjust to make the water behave more differently, but be careful as some configurations may cause the simulation to become unstable and diverge. Please read the tooltips carefully when customizing.
The WaterTrigger
component is also provided here to support automatic interactions with rigidbodies from Unity 2D Physics.
WaterTrigger
also handles buoyancy according to the state of WaterVolume
. Part of this is implemented by Unity's own Buoyancy Effector, which can also be adjusted separately to add flows and underwater damping. WaterTrigger
will also add an upward force near each wave, making objects float along waves.
Note that WaterTrigger
is in no way physically-accurate, as computing the exact submerged volume of each rigidbody would be way too computationally extensive.
MIT License. But, please note that ChocoWaterRenderFeature
is modified from DMeville's RefractedTransparentRenderPass (credited appropriately inside the same file).