When adding Directional or Point lights to a 3D scene in Unity's Universal Render Pipeline (URP), you may run into frustrating visual bugs: ugly dark striped lines or moire patterns crawling across mesh surfaces or the ground, jagged, pixelated shadow edges, or shadows detaching from the bases of characters and floating in the air after tweaking light parameters.
These issues are known as Shadow Acne and Peter Panning. They are classic, ubiquitous problems in 3D real-time rendering. This article explains the underlying rendering math behind these shadow artifacts and provides a step-by-step workflow to configure Bias (Depth Bias) and Normal Bias in URP for clean, realistic shadows.
1. Root Cause 1: Why Shadow Acne Occurs
The striped contour noise across flat or curved surfaces (known as Shadow Acne) is caused by self-shadowing errors where a mesh casts shadow on itself due to numerical precision limits.
To generate shadows, Unity renders the scene from the perspective of the light source into a specialized depth texture called a shadow map. Later, when rendering the main camera view, the GPU compares the depth of each pixel from the light source against the value stored in the shadow map: if the pixel's distance is greater than the shadow map value, the pixel is marked as shadowed.
Because the shadow map has a limited grid resolution, multiple screen pixels map to a single shadow map pixel. When lighting flat or sloped surfaces, precision differences and discretization error cause some pixels to evaluate as slightly closer than the shadow map, and others as slightly further away. This depth-testing jitter results in alternating dark and light pixels, appearing as crawling, striped moire artifacts on surfaces.
2. Root Cause 2: Why Peter Panning Occurs
The most straightforward way to erase shadow acne is to raise the light's Bias (Depth Bias). Bias introduces a small artificial offset that pushes the shadow map depth slightly away from the light source. This ensures that surface pixels always pass the shadow depth test, instantly cleaning up the surface stripes.
However, if you push the Bias too far, you cause Peter Panning.
Because the shadow depth is shifted too far behind the geometry, the shadow starts rendering with a physical gap underneath the object casting it, making characters and props look like they are floating in the air. To achieve professional graphics, developers must balance Bias and Normal Bias to eliminate acne while anchoring shadows firmly to the feet of objects.
3. Step-by-Step Bias Adjustment Workflow in URP
Under the "Shadows" settings in a URP Light component, you have access to two distinct types of bias offsets to resolve these artifacts.
| Property | Role & Mechanics | Adjustment Guidelines |
|---|---|---|
| Bias (Depth Bias) | Shifts the shadow map depth values uniformly along the light ray direction. Higher values lead directly to peter panning. | Keep as small as possible (e.g., 0.02–0.05). Maintain a tiny non-zero value to prevent baseline acne. |
| Normal Bias | Shifts the shadow map sampling coordinates slightly along the mesh surface normal vectors. Shrinks the shadow slightly along slopes without shifting the shadow depth backwards. | Increase this value first (e.g., 0.5–1.5). It is highly effective at clearing acne on rounded surfaces and steep slopes without detaching the shadow. |
Tuning Steps
- Place a character or sloped asset showing shadow acne close to the scene view.
- Set the Light's Bias and Normal Bias parameters both to
0(heavy acne will appear). - Slowly raise the Normal Bias in
0.1increments. For most scenes, the surface stripes will fade away between0.5and1.2. - If minor acne persists in crevices or flat corners, slowly increment the Bias by
0.01steps. Usually, a value between0.02and0.04will completely eliminate residual noise. - Check the contact points at the feet of your meshes to ensure no floating gaps (peter panning) have formed. If a gap is visible, decrease the Bias and compensate by slightly increasing the Normal Bias.
4. Resolving Jagged Shadow Edges in URP Asset settings
If your shadows are positioned correctly but look blocky or pixelated at the borders, your Shadow Map Resolution is insufficient. Adjust these settings in your project's URP Asset:
Universal Render Pipeline Asset in the Project window and open the "Lighting" and "Shadows" dropdowns in the Inspector.
- Shorten Shadow Distance:
The shadow map must cover the entire area up to the Shadow Distance setting. If this is set to a massive distance like150or200, shadow map texels are stretched thin, making close-up shadows look pixelated. Reducing this to30–50for close-up action games concentrates the shadow map resolution near the camera, dramatically sharpening your shadows. - Configure Shadow Cascades:
Cascades split the shadow distance into separate zones (e.g., Near, Mid, Far), assigning a dedicated slice of the shadow map to each. Setting this to2 Cascadesor4 Cascadesdistributes shadow resolution dynamically, keeping shadows crisp close to the camera while maintaining high performance. - Increase Shadow Atlas Resolution:
Increase the shadow resolution (e.g., from 1024 to 2048 or 4096). Note that higher resolutions consume more video memory (VRAM) and increase GPU overhead, so choose this wisely based on your target hardware.
5. Runtime Shadow Bias Optimizer Script (C#)
To dynamically adjust shadow biases depending on the target platform, camera FOV, or graphics settings, you can attach the following C# script to your light source. This is especially helpful on mobile platforms (Android/iOS) where shadow map resolutions must be lower, requiring wider biases, compared to PC/Console platforms where tighter biases are preferred.
using UnityEngine;
[RequireComponent(typeof(Light))]
[DisallowMultipleComponent]
public class URPShadowBiasOptimizer : MonoBehaviour
{
public enum TargetPlatformTier { MobileLow, MobileHigh, StandalonePC }
[Header("Graphics Quality Settings")]
public TargetPlatformTier platformTier = TargetPlatformTier.StandalonePC;
private Light targetLight;
void Start()
{
targetLight = GetComponent<Light>();
ApplyOptimalSettings();
}
public void ApplyOptimalSettings()
{
if (targetLight == null) return;
if (targetLight.shadows == LightShadows.None) return;
switch (platformTier)
{
case TargetPlatformTier.MobileLow:
// Low res shadows (e.g., 512): Use wider biases to prevent severe acne
// and push Near Plane out slightly to conserve depth buffer precision
targetLight.shadowBias = 0.08f;
targetLight.shadowNormalBias = 1.5f;
targetLight.shadowNearPlane = 0.5f;
break;
case TargetPlatformTier.MobileHigh:
// Mid-tier shadows (e.g., 1024)
targetLight.shadowBias = 0.04f;
targetLight.shadowNormalBias = 1.0f;
targetLight.shadowNearPlane = 0.2f;
break;
case TargetPlatformTier.StandalonePC:
// High-quality shadows (e.g., 2048+ with Soft Shadows)
// Keep biases extremely tight for realistic contact points
targetLight.shadowBias = 0.015f;
targetLight.shadowNormalBias = 0.4f;
targetLight.shadowNearPlane = 0.1f;
break;
}
Debug.Log($"[URPShadowBiasOptimizer] Optimized shadow bias settings for light '{name}' under {platformTier} quality.");
}
}
By attaching this to your main Directional Light, you can call ApplyOptimalSettings() during configuration loads to guarantee shadow quality and prevent rendering bugs across different hardware devices.