diff --git a/Runtime/DisplayXRLocal2D.cs b/Runtime/DisplayXRLocal2D.cs
index 6f9f078..a9e841c 100644
--- a/Runtime/DisplayXRLocal2D.cs
+++ b/Runtime/DisplayXRLocal2D.cs
@@ -198,7 +198,9 @@ private Vector2Int RtSize
/// The RenderTexture capturing the Canvas content.
public RenderTexture OverlayTexture { get; private set; }
- private static readonly Vector3 kCanvasWorldPos = new Vector3(0, 100000f, 0);
+ // This component's private stage (see DisplayXROverlayStage): its canvas and
+ // camera sit there so no other overlay's camera sees this canvas.
+ private int m_Stage = -1;
private const int kPrivateLayer = 30;
private Canvas m_Canvas;
@@ -310,7 +312,9 @@ void OnEnable()
// WorldSpace canvas parked far away on a private layer.
m_Canvas.renderMode = RenderMode.WorldSpace;
- m_CanvasRect.position = kCanvasWorldPos;
+ if (m_Stage < 0) m_Stage = DisplayXROverlayStage.Acquire();
+ Vector3 stagePos = DisplayXROverlayStage.Position(m_Stage);
+ m_CanvasRect.position = stagePos;
m_CanvasRect.rotation = Quaternion.identity;
m_CanvasRect.localScale = new Vector3(0.01f, 0.01f, 0.01f);
m_CanvasRect.sizeDelta = resolution;
@@ -321,7 +325,7 @@ void OnEnable()
var camGO = new GameObject("DisplayXR_Local2DCam");
camGO.transform.SetParent(transform, false);
camGO.hideFlags = HideFlags.HideAndDontSave;
- camGO.transform.position = kCanvasWorldPos + new Vector3(0, 0, 1);
+ camGO.transform.position = stagePos + new Vector3(0, 0, 1);
camGO.transform.rotation = Quaternion.LookRotation(Vector3.back, Vector3.down);
m_OverlayCamera = camGO.AddComponent();
@@ -459,6 +463,8 @@ private void ReleaseBridgeTex()
void OnDisable()
{
+ DisplayXROverlayStage.Release(m_Stage);
+ m_Stage = -1;
UnregisterContentDirtyTracking(); // #244 — drop Graphic dirty callbacks
ReleaseBridgeTex();
diff --git a/Runtime/DisplayXROverlayStage.cs b/Runtime/DisplayXROverlayStage.cs
new file mode 100644
index 0000000..148e9dd
--- /dev/null
+++ b/Runtime/DisplayXROverlayStage.cs
@@ -0,0 +1,47 @@
+// Copyright 2024-2026, DisplayXR contributors
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace DisplayXR
+{
+ ///
+ /// Private world-space "stage" positions for the offscreen canvases of
+ /// and .
+ ///
+ /// Each component parks its canvas far from the scene on a private layer and
+ /// renders it with its own ortho camera that culls to that layer. They all used
+ /// one shared spot, so with two or more overlays every camera also rendered the
+ /// other canvases: a HUD showed up inside the Local2D bubble and vice versa (the
+ /// layer can't separate them — they share it). Giving each live overlay its own
+ /// stage, spaced far beyond any canvas's extent and the cameras' 10-unit far
+ /// plane, keeps each camera on its own canvas.
+ ///
+ internal static class DisplayXROverlayStage
+ {
+ static readonly Vector3 kOrigin = new Vector3(0f, 100000f, 0f);
+ // A 4096-unit canvas at the 0.01 canvas scale is ~41 world units wide.
+ const float kSpacing = 1000f;
+
+ static readonly HashSet s_Used = new HashSet();
+
+ /// Take the lowest free stage index.
+ public static int Acquire()
+ {
+ int i = 0;
+ while (s_Used.Contains(i)) i++;
+ s_Used.Add(i);
+ return i;
+ }
+
+ /// Give a stage back. Negative indices are ignored.
+ public static void Release(int index)
+ {
+ if (index >= 0) s_Used.Remove(index);
+ }
+
+ /// World position of the canvas on stage .
+ public static Vector3 Position(int index) => kOrigin + new Vector3(index * kSpacing, 0f, 0f);
+ }
+}
diff --git a/Runtime/DisplayXROverlayStage.cs.meta b/Runtime/DisplayXROverlayStage.cs.meta
new file mode 100644
index 0000000..97e4cff
--- /dev/null
+++ b/Runtime/DisplayXROverlayStage.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 3061c7b5d24e44a2b50675d12268e867
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Runtime/DisplayXRWindowSpaceUI.cs b/Runtime/DisplayXRWindowSpaceUI.cs
index cffa097..646fb36 100644
--- a/Runtime/DisplayXRWindowSpaceUI.cs
+++ b/Runtime/DisplayXRWindowSpaceUI.cs
@@ -3,7 +3,7 @@
using UnityEngine;
using UnityEngine.Experimental.Rendering;
-using UnityEngine.Rendering;
+using UnityEngine.Rendering; // RenderPipelineManager, ScriptableRenderContext
namespace DisplayXR
{
@@ -110,7 +110,18 @@ public class DisplayXRWindowSpaceUI : MonoBehaviour
// We park the WorldSpace canvas at this fixed position, far from any
// scene content, so the dedicated camera looking at it sees nothing
// else that might bleed into our RT.
- private static readonly Vector3 kCanvasWorldPos = new Vector3(0, 100000f, 0);
+ // This component's private stage (see DisplayXROverlayStage): its canvas and
+ // camera sit there so no other overlay's camera sees this canvas.
+ private int m_Stage = -1;
+
+ // The URP transparent-overlay foreground clip (DisplayXR/ForegroundClipURP) is a
+ // full-screen pass on the renderer, so it also runs for this overlay camera and,
+ // keyed on _DXRForegroundFar (z>0.5 = armed), discards the whole canvas: the RT
+ // comes out fully transparent and the HUD never shows. Same fix as
+ // DisplayXRLocal2D: zero the global for our camera's render, restore after.
+ private static readonly int s_ForegroundFarId = Shader.PropertyToID("_DXRForegroundFar");
+ private Vector4 m_SavedForegroundFar;
+ private bool m_CamRenderHooked;
// Dedicated layer: we put the canvas + children on this layer and give
// ONLY our overlay camera that layer in its cullingMask. We pick a
// mid-range layer that's typically unused (Unity reserves 0-7 for
@@ -185,7 +196,9 @@ void OnEnable()
m_Canvas.renderMode = RenderMode.WorldSpace;
// worldCamera is assigned to the OverlayCamera below (after creation)
// so GraphicRaycaster can project screen-cursor input onto the canvas.
- m_CanvasRect.position = kCanvasWorldPos;
+ if (m_Stage < 0) m_Stage = DisplayXROverlayStage.Acquire();
+ Vector3 stagePos = DisplayXROverlayStage.Position(m_Stage);
+ m_CanvasRect.position = stagePos;
m_CanvasRect.rotation = Quaternion.identity;
// Use the canvas's existing reference width as the scale baseline.
// 1 world unit per UI unit at scale 1 → set scale so the RT
@@ -208,7 +221,7 @@ void OnEnable()
// pipeline and the swapchain image our native blit feeds the
// runtime compositor with. Without this the panel reads
// upside-down in the runtime preview window.
- camGO.transform.position = kCanvasWorldPos + new Vector3(0, 0, 1);
+ camGO.transform.position = stagePos + new Vector3(0, 0, 1);
camGO.transform.rotation = Quaternion.LookRotation(Vector3.back, Vector3.down);
m_OverlayCamera = camGO.AddComponent();
@@ -236,6 +249,12 @@ void OnEnable()
// Mirrors DisplayXRLocal2D (which was migrated off manual Render for the
// canvas-rebuild race — same enabled=true resolution).
m_OverlayCamera.enabled = true;
+ if (!m_CamRenderHooked)
+ {
+ RenderPipelineManager.beginCameraRendering += OnBeginOverlayCamera;
+ RenderPipelineManager.endCameraRendering += OnEndOverlayCamera;
+ m_CamRenderHooked = true;
+ }
// Wire OverlayCamera as the canvas's event camera. GraphicRaycaster
// needs a camera reference to project screen-cursor input onto a
@@ -450,9 +469,34 @@ private void ReleaseBridgeTex()
m_BridgePtr = System.IntPtr.Zero;
}
+ // Scope the URP foreground clip out of our overlay camera (see the field doc).
+ // Camera-identity gated, so other cameras are untouched.
+ void OnBeginOverlayCamera(ScriptableRenderContext ctx, Camera cam)
+ {
+ if (cam != m_OverlayCamera) return;
+ m_SavedForegroundFar = Shader.GetGlobalVector(s_ForegroundFarId);
+ if (m_SavedForegroundFar.z > 0.5f)
+ Shader.SetGlobalVector(s_ForegroundFarId, Vector4.zero);
+ }
+
+ void OnEndOverlayCamera(ScriptableRenderContext ctx, Camera cam)
+ {
+ if (cam != m_OverlayCamera) return;
+ if (m_SavedForegroundFar.z > 0.5f)
+ Shader.SetGlobalVector(s_ForegroundFarId, m_SavedForegroundFar);
+ }
+
void OnDisable()
{
ReleaseSlot();
+ if (m_CamRenderHooked)
+ {
+ RenderPipelineManager.beginCameraRendering -= OnBeginOverlayCamera;
+ RenderPipelineManager.endCameraRendering -= OnEndOverlayCamera;
+ m_CamRenderHooked = false;
+ }
+ DisplayXROverlayStage.Release(m_Stage);
+ m_Stage = -1;
// Restore the canvas's original mode + transform + layer.
if (m_StateSaved && m_Canvas != null)