Quick answer: SpriteMask fails in builds when the SpriteRenderer.maskInteraction is set to None, the mask’s Sorting Layer range does not cover the target renderer’s layer, the render pipeline lacks stencil buffer support, or the depth buffer format strips the stencil bits. Set Mask Interaction to Visible Inside Mask, verify sorting layer ranges, and confirm your URP 2D Renderer has stencil enabled.

Here is how to fix Unity SpriteMask not masking in builds. Everything looks correct in the Scene view and Game view within the editor. The mask clips sprites exactly as intended. You build the project, run the executable, and the mask has no effect — all sprites render fully visible as if the SpriteMask does not exist. No errors, no warnings, just broken masking.

The Symptom

A SpriteMask component clips or reveals sprites correctly in the Unity editor (both Scene view and Play mode). After building to a target platform (Windows, Android, iOS, WebGL), the masking effect disappears entirely. Sprites that should be hidden by the mask render fully, or sprites that should be visible only inside the mask render everywhere.

The SpriteMask GameObject is active, the sprite assigned to the mask is included in the build, and no script is disabling the component at runtime.

What Causes This

Mask Interaction set to None. Each SpriteRenderer that should respond to a mask must have its Mask Interaction property set to either Visible Inside Mask or Visible Outside Mask. The default is None, which means the renderer ignores all masks. The editor may appear to work due to rendering order coincidences in the Game view, but builds enforce this strictly.

Sorting Layer range mismatch. The SpriteMask component has Front Sorting Layer and Back Sorting Layer properties that define which layers it affects. If your target SpriteRenderer lives on a sorting layer outside this range, the mask has no effect on it.

Missing stencil buffer. SpriteMasks use the stencil buffer to perform clipping. If the graphics API or render pipeline is configured without a stencil buffer (depth buffer format less than D24S8), masking cannot function. This is more common on mobile with aggressive quality settings or custom render pipeline configurations.

URP Universal Renderer instead of 2D Renderer. In URP, SpriteMask is designed for the 2D Renderer. Using the Universal Renderer (forward or deferred) with 2D sprites can cause stencil conflicts where masks do not apply correctly because the stencil buffer is already in use for other effects.

The Fix

Step 1: Set Mask Interaction on every affected renderer.

// Set at runtime if needed
SpriteRenderer sr = GetComponent<SpriteRenderer>();
sr.maskInteraction = SpriteMaskInteraction.VisibleInsideMask;

// Or for revealing outside the mask:
sr.maskInteraction = SpriteMaskInteraction.VisibleOutsideMask;

Step 2: Configure the SpriteMask sorting layer range. Select the SpriteMask component. Under “Front Sorting Layer” and “Back Sorting Layer,” ensure the range encompasses the sorting layers of all renderers you want masked. Setting both to “Default” only masks renderers on the Default sorting layer.

// Verify mask range covers the target layers
SpriteMask mask = GetComponent<SpriteMask>();
Debug.Log($"Mask range: {mask.frontSortingLayerID} to {mask.backSortingLayerID}");
Debug.Log($"Target renderer layer: {targetRenderer.sortingLayerID}");

Step 3: Verify stencil buffer availability. In URP, open your Universal Renderer Data asset. Check that Depth Texture is enabled and the depth buffer format includes stencil bits (Depth 24 Stencil 8 or Depth 32 Stencil 8). On mobile, some quality levels reduce this.

Step 4: Use the 2D Renderer for 2D projects. If you are using URP for a 2D game, switch from the Universal Renderer to the 2D Renderer asset. The 2D Renderer has proper SpriteMask support built into its rendering path and avoids stencil conflicts with 3D rendering features.

Testing Across Platforms

SpriteMask behavior is consistent across platforms when configured correctly, but the failure mode differs. On WebGL, a missing stencil buffer silently fails. On Android with Vulkan, stencil buffer allocation depends on the surface format requested. Always test masking in an actual build rather than relying on editor behavior.

“The editor lies about masking. It can look correct even with maskInteraction set to None. The build does not lie.”

Multiple Masks and Interactions

When using multiple overlapping SpriteMasks, each mask operates independently via stencil buffer increments. If two masks overlap and a renderer is set to VisibleInsideMask, it renders where either mask covers. To combine masks with AND logic (visible only where both masks overlap), you need a custom stencil shader approach. The built-in system uses OR logic for overlapping masks.

// Check mask state at runtime for debugging
SpriteMask[] masks = FindObjectsOfType<SpriteMask>();
foreach (var mask in masks)
{
    Debug.Log($"Mask: {mask.name}, sprite={mask.sprite != null}, " +
        $"front={mask.frontSortingLayerID}, back={mask.backSortingLayerID}");
}

Alternative: Shader-Based Masking

For complex masking needs that go beyond what SpriteMask offers (soft edges, arbitrary shapes, animated masks), consider using a custom shader with _StencilComp and _StencilOp properties or a shader-based dissolve/clip approach. This avoids the sorting layer range limitations entirely but requires shader knowledge.

Always set Mask Interaction explicitly. The default of None means the renderer opts out of masking entirely, even if a mask is right on top of it.