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)