Create an Animated Cartoon Water Shader for VRChat
This tutorial creates an unlit water shader for Unity's Built-in Render Pipeline. It samples one grayscale noise texture twice, scrolls the samples in different directions using Unity's built-in _Time value, and blends between two material colors.
The result is a simple animated surface. It does not calculate depth, shoreline foam, reflections, refraction, or physical waves.
A two-color animated material for a flat water mesh in a VRChat world.
- Create an Unlit Shader asset and replace its contents with the shader below.
- Create a material that uses
VRCreators/Simple Cartoon Water. - Assign a repeating grayscale noise texture, then set the colors, tiling, speed, and opacity.
VRChat's official setup guide says not to use URP or HDRP. This tutorial therefore uses a Built-in Render Pipeline vertex and fragment shader, not Shader Graph.
Before You Begin
You need:
- a VRChat world project created with the Creator Companion
- a flat mesh with usable UV coordinates, such as a Unity Plane
- a seamless grayscale noise texture
- a text editor configured for Unity shader files
Select the noise texture in the Project window and set Wrap Mode to Repeat in its Texture Import Settings. The shader moves its UV coordinates beyond the texture's original 0–1 range, so repeating edges keep the animation continuous.
1. Create the Shader Asset
In the Project window:
- Select Create → Shader → Unlit Shader.
- Name the asset
SimpleCartoonWater. - Double-click it to open the shader source.
- Replace the generated code with the following:
Shader "VRCreators/Simple Cartoon Water"
{
Properties
{
_ColorA ("Deep Color", Color) = (0.02, 0.25, 0.45, 1)
_ColorB ("Light Color", Color) = (0.10, 0.75, 0.85, 1)
_NoiseTex ("Noise Texture", 2D) = "gray" {}
_Tiling ("Noise Tiling", Range(0.1, 10)) = 2
_Speed ("Scroll XY / ZW", Vector) = (0.03, 0.02, -0.02, 0.01)
_Opacity ("Opacity", Range(0, 1)) = 0.75
}
SubShader
{
Tags
{
"Queue" = "Transparent"
"RenderType" = "Transparent"
"IgnoreProjector" = "True"
}
Cull Back
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
};
sampler2D _NoiseTex;
fixed4 _ColorA;
fixed4 _ColorB;
float _Tiling;
float4 _Speed;
float _Opacity;
v2f vert(appdata input)
{
v2f output;
output.vertex = UnityObjectToClipPos(input.vertex);
output.uv = input.uv * _Tiling;
return output;
}
fixed4 frag(v2f input) : SV_Target
{
float time = _Time.y;
float firstNoise = tex2D(
_NoiseTex,
input.uv + (_Speed.xy * time)
).r;
float secondNoise = tex2D(
_NoiseTex,
(input.uv * 1.37) + (_Speed.zw * time)
).g;
float pattern = saturate((firstNoise + secondNoise) * 0.5);
fixed4 color = lerp(_ColorA, _ColorB, pattern);
color.a = _Opacity;
return color;
}
ENDCG
}
}
Fallback Off
}
Save the file and return to Unity. Wait for Unity to finish compiling before creating the material.
2. Create and Assign the Material
- Select Create → Material in the Project window.
- Name the asset
MAT_CartoonWater. - In the Material Inspector, open the Shader dropdown.
- Select VRCreators → Simple Cartoon Water.
- Assign the noise texture to Noise Texture.
- Assign the material to the water mesh's Mesh Renderer.
If the shader does not appear in the menu, check the Console. A shader compile error includes the affected file and line.
3. Configure the Water
| Property | Function | Starting value |
|---|---|---|
| Deep Color | Color used for darker noise values | Dark blue or teal |
| Light Color | Color used for lighter noise values | Lighter blue, cyan, or green |
| Noise Texture | Grayscale pattern sampled twice by the shader | Seamless noise with Wrap Mode set to Repeat |
| Noise Tiling | Number of times the pattern repeats across the mesh UVs | 2 |
| Scroll XY / ZW | XY controls the first sample; ZW controls the second | (0.03, 0.02, -0.02, 0.01) |
| Opacity | Alpha returned by the fragment shader | 0.75 on PC |
Small speed values create slow movement. A negative value reverses that sample's direction. The two samples use different scale and direction values so the pattern does not move as a single flat image.
Because this is an unlit shader, Unity lights do not change its colors. Set the two colors to values that already fit the scene.
4. Place the Water Mesh
Place the mesh where the water surface should appear and check the following:
- The visible side of the mesh faces upward. This shader uses
Cull Back. - The mesh UVs cover the surface without unwanted stretching.
- The water does not intersect other transparent surfaces unnecessarily.
- The material remains readable from the player's normal viewing distance.
This shader changes only the rendered surface. Add a separate collider if the water needs to block objects, and use world logic if entering the water should trigger an effect.
Make an Opaque Android Variant
VRChat warns that transparency has a significant alpha fill-rate cost on mobile GPUs. When see-through water is not required, duplicate the shader and material for the Android build, then make these changes:
Tags
{
"Queue" = "Geometry"
"RenderType" = "Opaque"
}
Cull Back
ZWrite On
Blend Off
In the fragment function, replace:
color.a = _Opacity;
with:
color.a = 1;
The colors and scrolling noise remain animated, but the surface is fully opaque and writes to the depth buffer.
What Each Shader Section Does
| Section | Purpose |
|---|---|
Properties |
Exposes colors, texture, tiling, speeds, and opacity in the Material Inspector |
Tags |
Places the transparent version in Unity's transparent render queue |
ZWrite Off |
Stops the transparent surface from writing depth |
Blend SrcAlpha OneMinusSrcAlpha |
Enables conventional alpha blending |
vert |
Transforms mesh vertices and passes scaled UV coordinates to the fragment shader |
_Time.y |
Supplies elapsed scaled time for the scrolling animation |
frag |
Samples the noise texture, blends the two colors, and returns the final pixel color |
The material is bright pink.
Open the Console and fix the first shader compile error. Also confirm the project uses Unity's Built-in Render Pipeline; VRChat does not use URP or HDRP.
The water is a single flat color.
Assign a grayscale noise texture to Noise Texture. The built-in gray default contains no variation, so both shader samples return the same value.
The animation has visible seams.
Use a seamless noise image, set its Wrap Mode to Repeat, and click Apply in the Texture Import Settings.
The water disappears when viewed from below.
The shader culls back-facing triangles. Use a mesh with correctly oriented faces, or create separate underside geometry when the water must be visible from below.
Transparent objects sort incorrectly through the water.
Alpha-blended surfaces can have draw-order problems, especially when they overlap. Reduce overlapping transparent geometry or use the opaque variant where transparency is not required.
The Android version is too expensive.
Use the opaque variant, reduce the visible water area, and test the Android build on target hardware. VRChat recommends avoiding transparency for Android content where possible.
Official References
- VRChat Creation: Getting Started — do not use URP or HDRP
- VRChat Creation: Android Content Optimization
- Unity Manual 2022.3: Vertex and fragment shader examples
- Unity Manual 2022.3: Defining material properties
- Unity Manual 2022.3: Built-in shader variables
- Unity Manual 2022.3: ShaderLab Blend command
- Unity Manual 2022.3: Texture Import Settings
Continue Learning
- Create and Apply a Solid-Color Material in Unity
- Create and Manage Materials
- Unity Performance Basics for VRChat Worlds
- Set Up a Skybox and Environment Lighting in Unity