Make a Stylized Water Shader in Unity Shader Graph (URP)

Illustration: stylized holographic ocean waves with a wireframe grid

Good-looking water does more for a scene than almost any other single effect, and you don’t need a custom HLSL ocean system to get it. This Unity Shader Graph water tutorial builds a stylized URP water shader step by step: shallow-to-deep colour from the depth buffer, two scrolling normal maps, foam lines where the water meets the shore, cheap refraction, and gentle vertex waves. Every part is optional, so you can keep only what your target platform can afford.

Project and URP Setup

Stylized water reads two things the camera produces before transparent objects are drawn: the depth texture (how far away the ground under the water is) and the opaque texture (a copy of the screen, used for refraction). Both are off by default in many URP assets.

  1. Select your active URP Asset (Project Settings > Graphics, then the asset assigned for your current Quality level).
  2. Tick Depth Texture. You need this for depth colour and foam.
  3. Tick Opaque Texture only if you want refraction. Leave Opaque Downsampling at 2x Bilinear for a cheaper, slightly blurred copy, which actually suits water.

Each Quality level can use a different URP asset, so check all of them. A setting enabled on the “High” asset does nothing if your build runs on “Medium”. If you’re new to Shader Graph itself, start with our Shader Graph beginner’s guide and come back.

For the water mesh, a subdivided plane works best. Unity’s built-in Plane has only 10×10 quads, which is enough for flat water but too coarse for vertex waves. Use a plane from ProBuilder with more subdivisions, or export a grid from Blender.

Create the Water Graph

Right-click in the Project window and choose Create > Shader Graph > URP > Lit Shader Graph. Name it StylizedWater. In the Graph Inspector’s Graph Settings:

  • Surface Type: Transparent. Water must be transparent so the depth and opaque textures contain the scene underneath it, not the water itself.
  • Blending Mode: Alpha.
  • Render Face: Front.
  • Leave Depth Write on its default (off for transparent).

Create a material from the graph (right-click the graph, Create > Material) and drop it on your plane. Add these properties on the Blackboard as you go: ShallowColor and DeepColor (Color), DepthDistance (Float, default 2), NormalMap (Texture2D, set Mode to Normal Map), NormalStrength (Float, 0.5), RippleSpeed (Float, 0.05), FoamDistance (Float, 0.4), FoamColor (Color, white).

Depth Colour: Shallow to Deep

The core trick of stylized water is measuring how much water sits between the surface and the ground behind it. Shallow parts get a light turquoise, deep parts a dark blue.

  1. Add a Scene Depth node with Sampling Mode Eye. This is the distance from the camera to the opaque ground, in world units.
  2. Add a Screen Position node with Mode Raw, then a Split node. The A (w) output is the eye depth of the water surface pixel itself.
  3. Subtract: Scene Depth minus Screen Position.A. The result is the water thickness along the view ray.
  4. Divide by DepthDistance, then Saturate to clamp it to 0..1.
  5. Lerp from ShallowColor (A) to DeepColor (B) using that value as T. Plug the RGB into Base Color.

For alpha, give your colours their own alpha (shallow around 0.4, deep around 0.9) and use the Lerp’s A channel through a Split node into the master stack’s Alpha. Now the water is see-through near the shore and opaque out deep, which is exactly what real shallow water looks like.

Smoothness around 0.9 and Metallic 0 give a nice specular highlight from your directional light. Stylized water often looks best with a soft, broad highlight rather than a pin-point one, so experiment.

Scrolling Normals for Ripples

A flat surface with a fixed highlight looks like plastic. Two normal maps scrolling in different directions make the highlight break up and shimmer.

  1. Add a Position node set to World, then a Swizzle (or Split and Vector 2) to get XZ. Using world XZ as UVs means the ripples stay the same size no matter how big you scale the plane, and adjacent tiles line up.
  2. Multiply by a tiling value (0.1 is a good start).
  3. Add a Time node, multiply Time by RippleSpeed, and feed it into a Tiling And Offset node’s Offset, with the XZ UVs as input.
  4. Sample Texture 2D with your NormalMap, Type set to Normal.
  5. Duplicate the chain with a different tiling (for example 0.17) and a negative or rotated offset, so the two layers move against each other.
  6. Combine them with a Normal Blend node, then a Normal Strength node driven by NormalStrength, into the master stack’s Normal (Tangent Space).

Any tileable water normal map works. If you don’t have one, a Normal From Height node fed by a Gradient Noise also works, though it’s more expensive than a texture and slightly blurrier.

Foam Lines at the Shore

Foam reuses the water thickness you already computed. Where the thickness is very small, the surface is touching geometry: rocks, shorelines, pillars.

  1. Take the thickness (Scene Depth minus Screen Position.A) again and divide it by FoamDistance, then Saturate.
  2. One Minus it, so pixels close to geometry are 1 and open water is 0.
  3. For a cartoon look, add a Step node with Edge around 0.5. You get a crisp band instead of a gradient.
  4. For animated foam, multiply the result by a scrolling Gradient Noise (scale about 20, using the same world-XZ and Time setup) before the Step, so the edge wobbles.
  5. Lerp your water colour toward FoamColor using the foam mask, and also Maximum the alpha with the mask so foam stays opaque.

Depth foam only appears where opaque geometry is behind the water. It won’t show around other transparent objects or particles, because they aren’t in the depth texture.

🌊 Skip the node spaghetti?

If you need ocean-scale water with buoyancy, caustics and underwater effects, a dedicated water asset can save weeks. We list the ones worth buying in our tools roundup.

See our Asset Store picks →

Refraction With Scene Color

Refraction bends whatever is under the water. With Opaque Texture enabled:

  1. Take a Screen Position node in Default mode (normalized 0..1 screen UVs).
  2. Take the XY of your blended normal, multiply by a small RefractionStrength (0.02 to 0.05), and Add it to the screen position.
  3. Feed the result into a Scene Color node.
  4. Lerp between Scene Color and your depth colour using the depth value, then plug that into Base Color. Because you are now drawing the background yourself, you can set Alpha to 1 for the refracted version, or keep partial alpha for a mix.

A known artefact: objects in front of the water bleed into the refraction near their edges, because the offset UV samples pixels from the foreground object. Keep the strength small, or sample Scene Depth at the offset UV and fall back to the un-offset UV when that depth is closer than the water surface.

A cheap win: in an Unlit graph, Scene Color plus fake lighting often looks better on stylized water than full Lit, and it skips the lighting cost.

Vertex Waves

Gentle swells come from moving vertices in the Vertex stage. Keep it simple: a couple of sine waves.

  1. Add a Position node in World space, take X and Z.
  2. Compute sin(X * frequency + Time * speed) and sin(Z * frequency * 0.8 + Time * speed * 1.3), add them, and multiply by an Amplitude property (0.1 to 0.3).
  3. Add that to the world Y, build a new Vector 3, then convert it back with a Transform node from World to Object, and plug it into the master stack’s Vertex Position.

Using world position keeps waves continuous across multiple water tiles. Two caveats: the vertex normals don’t change, so lighting won’t follow the waves unless you also compute a normal (the ripple normals usually hide this), and the mesh’s bounds don’t grow, so keep amplitude small to avoid frustum culling clipping the edges.

If gameplay needs to know the wave height (boats bobbing, floating crates), compute the same sine formula in C# with Time.time. Shader Graph’s Time node uses the same timeline as Time.time in Play mode, so the results line up:

using UnityEngine;

public class SimpleBuoyancy : MonoBehaviour
{
    [SerializeField] float waterLevel = 0f;
    [SerializeField] float amplitude = 0.2f;
    [SerializeField] float frequency = 0.5f;
    [SerializeField] float speed = 1f;

    public float WaveHeight(Vector3 p)
    {
        float t = Time.time * speed;
        float w = Mathf.Sin(p.x * frequency + t)
                + Mathf.Sin(p.z * frequency * 0.8f + t * 1.3f);
        return waterLevel + w * amplitude;
    }

    void Update()
    {
        Vector3 pos = transform.position;
        pos.y = WaveHeight(pos);
        transform.position = pos;
    }
}

Match these values to the material’s properties. For physics-driven floating, apply an upward force with a Rigidbody in FixedUpdate when the object is below WaveHeight instead of setting position directly, and see our Rigidbody jitter guide if the boat starts shaking.

Making It Run on Mobile

Water covers a lot of screen, so every pixel of cost multiplies. In rough order of impact:

  • Drop refraction first. The Opaque Texture is a full-screen copy every frame. Turning it off in the mobile URP asset is the single biggest saving.
  • Use one normal map instead of two, or a lower resolution one (256 or 512 is plenty for stylized water).
  • Use an Unlit graph with a fake specular (a Fresnel Effect node tinted white) instead of full Lit.
  • Keep the depth texture if you can. Depth colour and foam carry most of the look. On some older GPUs, URP has to do an extra depth prepass to produce it, so profile on a real device.
  • Use Gradient Noise sparingly. It’s procedural and costs more math per pixel than a texture sample. Bake noise into a texture’s spare channel if you need it.
  • Use Material keywords (a Boolean Keyword on the Blackboard) to compile out refraction and foam per platform, rather than keeping two graphs.

Our Profiler guide covers how to confirm the water is the actual bottleneck before you start cutting features.

Troubleshooting

The water is one flat colour with no depth gradient

Depth Texture is off in the URP asset being used, or the material is Opaque. Enable Depth Texture on every Quality level’s URP asset and set the graph’s Surface Type to Transparent. Also check the camera’s own Depth Texture setting is “Use Pipeline Settings” rather than Off.

Scene Color is grey or black

Opaque Texture is disabled, either in the URP asset or on the camera. Turn it on, and make sure the material is Transparent, since opaque materials draw before the copy is made.

Foam appears everywhere or nowhere

Your FoamDistance is wrong for your scene scale, or you used Scene Depth in Linear01 mode while subtracting eye depth. Use Eye mode for both sides of the subtraction.

Water edges flicker or ripples stretch

Mesh UVs are stretched on a scaled plane. Switch to world-space XZ UVs as shown above.

Waves look like steps

The mesh doesn’t have enough vertices. Waves can only move vertices that exist, so subdivide the plane.

Water is pink

The graph targets the wrong pipeline or failed to compile. See our pink materials fix.

FAQ

Does Unity have built-in water for URP?

HDRP has a full water system. URP doesn’t ship one, so stylized URP water is usually a custom Shader Graph like this one or an Asset Store package.

Lit or Unlit graph for water?

Lit gives real specular highlights and reacts to scene lighting. Unlit is cheaper and gives you full control over the stylized look. On mobile, Unlit with a Fresnel highlight is usually the better trade.

Why does my water need to be transparent?

The depth and opaque textures are captured after opaque objects and before transparent ones. A transparent water surface can read the scene beneath it. An opaque one can’t.

Can I make water reflect the sky?

Yes. A Lit graph with high smoothness reflects the scene’s reflection probe or skybox automatically. For true planar reflections you need an extra camera or a third-party solution, which is expensive.

How do I make objects float on the waves?

Recompute the same wave formula in C# using Time.time, then move objects or apply buoyancy forces based on that height.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *