Quick answer: Shader Graph vertex displacement only runs in the visible color pass. The Shadow Caster pass uses the original mesh, so shadows are cast from the undisplaced silhouette. Wrap your displacement math in a Custom Function node and feed it into the Vertex Position output of every sub-target (ShadowCaster, DepthOnly, MotionVectors), enable two-sided shadows on the renderer, and your shadow will track the displaced mesh exactly.
You build a beautiful wind-blown grass shader in Shader Graph. Vertex displacement sways the blades convincingly. You drop a directional light into the scene and the shadows on the ground are still completely flat and rigid — like the grass is not moving at all. The renderer is technically right: Unity is dutifully rendering the shadow of a mesh whose vertices have not changed, because the shadow caster pass is a different shader entirely.
The Symptom
You enable vertex animation in a Shader Graph — could be wind sway, water displacement, breathing creatures, melting effects, anything that writes to the Vertex Position output. The visible mesh deforms exactly as you expect. The shadow on the ground or against another surface stays in the rest pose. From some angles the visible mesh and the shadow appear to belong to two different objects.
If you open the Frame Debugger and step through the passes for one frame, you can see the issue concretely. The ShadowCaster pass renders the mesh with a stripped-down vertex shader. The vertex positions used there match the imported mesh, not your displaced output. Your modification only takes effect during the ForwardLit or UniversalForward pass.
What Causes This
Unity’s rendering pipeline runs each mesh through several passes per frame. The shadow map for a directional light is filled by every shadow-casting object running its ShadowCaster pass: a minimal vertex shader that writes only depth. By default this pass uses the standard vertex transform — UnityObjectToClipPos(input.positionOS) — with no awareness of Shader Graph nodes connected to the master Vertex Position output.
Shader Graph compiles a separate sub-shader per render pass. The Vertex stack in the master block is shared across passes only if every active sub-target opts in. In URP and HDRP, the ShadowCaster, DepthOnly, DepthNormalsOnly, and MotionVectors sub-targets either use a placeholder vertex stage or a reduced one. If your displacement depends on a Custom Function node or sample-from-texture node that the shadow sub-target does not include, the displacement silently disappears in shadows.
Two related causes commonly show up alongside this:
1. Backface culling on displaced surfaces. When displacement pushes vertices outward, the implied face normals can become inconsistent. The visible pass does not care; the shadow caster culls backfaces and produces shadow holes.
2. Motion vector mismatch. If your project uses TAA or motion blur, the motion vectors are computed from the previous-frame and current-frame vertex positions in the MotionVectors pass. If only the color pass sees the displacement, the motion vectors point to the wrong screen-space delta, which produces ghosting on TAA.
The Fix
Step 1: Confirm the diagnosis with the Frame Debugger. Open Window > Analysis > Frame Debugger, enable it, and step through the shadow pass for your light. Verify the displaced object renders into the shadow map using the un-displaced mesh.
Step 2: Encapsulate displacement in a reusable function. Move your displacement math into a Custom Function node so the same code runs in every sub-target. Define a single HLSL include that all passes can use.
// File: Assets/Shaders/WindDisplacement.hlsl
// Single source of truth for the displacement math.
#ifndef WIND_DISPLACEMENT_INCLUDED
#define WIND_DISPLACEMENT_INCLUDED
void ApplyWind_float(
float3 positionOS,
float3 normalOS,
float windStrength,
float time,
out float3 displacedPositionOS) {
// Sway only the upper portion of the mesh (grass blade tip).
float heightMask = saturate(positionOS.y);
float phase = positionOS.x * 0.5 + positionOS.z * 0.5;
float3 sway = float3(
sin(time * 2.0 + phase),
0,
cos(time * 1.7 + phase)
) * windStrength * heightMask;
displacedPositionOS = positionOS + sway;
}
#endif
In Shader Graph, drop a Custom Function node, point it at the file, choose ApplyWind as the function name, then connect its output to the Vertex Position block. Because all sub-targets share the master Vertex stack when they opt in, the displacement now runs everywhere.
Step 3: Force every active sub-target to use the Vertex stack. In the Shader Graph Blackboard, open the Graph Inspector and confirm that ShadowCaster, DepthOnly, DepthNormals, and MotionVectors are enabled in the active sub-targets list. If any pass shows “Vertex Position: Default” instead of inheriting the master block, manually override it.
// In a hand-authored URP Lit shader (HLSL), the equivalent is:
Pass {
Name "ShadowCaster"
Tags { "LightMode" = "ShadowCaster" }
Cull Off // Two-sided shadows for displaced surfaces.
HLSLPROGRAM
#pragma vertex ShadowPassVertex
#pragma fragment ShadowPassFragment
#include "WindDisplacement.hlsl"
Varyings ShadowPassVertex(Attributes input) {
Varyings o;
float3 displaced;
ApplyWind_float(input.positionOS.xyz, input.normalOS,
_WindStrength, _Time.y, displaced);
o.positionCS = TransformObjectToHClip(displaced);
return o;
}
ENDHLSL
}
Step 4: Enable two-sided shadows. On the MeshRenderer, set Cast Shadows to Two Sided. In the shader, set Cull Off for the ShadowCaster pass. This stops shadow holes when displacement flips face winding near silhouettes.
Step 5: Wire the same displacement into MotionVectors. If you use TAA, dynamic resolution, or motion blur, the motion vector pass needs displacement too. Otherwise TAA will smear because the previous-frame pixel does not match where the renderer thinks it came from. The Motion Vectors sub-target in URP 14+ accepts the same Vertex Position connection.
Step 6: Verify in the Frame Debugger again. Step through the ShadowCaster pass for your displaced object. The shadow map should now reflect the swaying mesh.
Why This Works
Shadow casting is a depth-only render pass. It runs the vertex shader, writes depth, and discards everything else. Because it is a separate pass, it has its own vertex transform, and Shader Graph treats each pass independently. By piping the displacement through a shared HLSL include, you guarantee that every pass — not just the visible one — transforms vertices identically.
Two-sided rendering matters because vertex displacement can violate the assumption that face normals point outward consistently. Disabling cull for the shadow caster trades a small amount of shadow map fill rate for correct silhouettes on displaced geometry.
Motion vectors complete the picture. Anything that animates per-vertex must report that animation to the motion vectors pass or temporal techniques will treat it as if the mesh had teleported between frames.
"Shader Graph compiles a separate vertex stage per pass. If your displacement is not wired into ShadowCaster, DepthOnly, and MotionVectors, your beautiful animated mesh casts the shadow of a statue."
Related Issues
If your bake-time lighting also looks off after enabling vertex animation, see Fix: Unity Baked Lightmap Looks Wrong. For seam artifacts on tiled vertex displacement, check Fix: Unity Baked Lighting Seams on Modular Meshes.
If a pass does not run your code, it does not see your geometry.