Files
2026-11-01 23:39:55 +02:00

402 lines
22 KiB
C#

using UnityEngine.Experimental.Rendering;
using UnityEngine.Rendering.RenderGraphModule;
#if ENABLE_UIELEMENTS_MODULE
using UnityEngine.UIElements;
#endif
namespace UnityEngine.Rendering.Universal
{
/// <summary>
/// Draw screen space overlay UI into the given color and depth target
/// </summary>
internal class DrawScreenSpaceUIPass : ScriptableRenderPass
{
// Renderer-owned materials (created and destroyed by the renderer, not this pass).
readonly Material m_UIBackdropFilterCompositeMaterial;
// Blits each camera's post-FX color into the composite buffer.
readonly Material m_SeedBlitMaterial;
const int k_CompositePassIndex = 0;
bool m_RequiresComposition;
// Backdrop-filter overlay buffers, owned by this pass; null on passes that never composite (e.g. 2D).
RTHandle m_OverlayCompositeBuffer; // overlay UI renders here, then blits to the backbuffer
RTHandle m_OverlayCompositeBufferBefore; // pre-UI snapshot the composite diffs against
RTHandle m_OverlayDepthStencilBuffer; // depth/stencil for the overlay UI (stencil clipping)
// Per-frame render-graph handles, re-imported each frame (or null when the composite path is skipped).
TextureHandle m_OverlayCompositeBufferHandle = TextureHandle.nullHandle;
TextureHandle m_OverlayCompositeBufferBeforeHandle = TextureHandle.nullHandle;
TextureHandle m_OverlayDepthStencilBufferHandle = TextureHandle.nullHandle;
/// <summary>
/// Creates a new <c>DrawScreenSpaceUIPass</c> instance.
/// </summary>
/// <param name="evt">The <c>RenderPassEvent</c> to use.</param>
/// <param name="uiBackdropFilterCompositeMaterial">Material for the diff composite blit, created and owned by the renderer. Pass null when this pass instance never runs the unsafe overlay path.</param>
/// <param name="seedBlitMaterial">URP blit material for the per-camera scene-to-buffer seed step, created and owned by the renderer. Pass null when this pass instance never runs the unsafe overlay path.</param>
/// <seealso cref="RenderPassEvent"/>
public DrawScreenSpaceUIPass(RenderPassEvent evt, Material uiBackdropFilterCompositeMaterial = null, Material seedBlitMaterial = null)
{
profilingSampler = URPProfilingSamplers.DrawScreenSpaceUI;
renderPassEvent = evt;
m_UIBackdropFilterCompositeMaterial = uiBackdropFilterCompositeMaterial;
m_SeedBlitMaterial = seedBlitMaterial;
}
// Computes this frame's composite need; the renderer calls it each frame like other passes' Setup().
internal void Setup(UniversalCameraData cameraData)
{
#if ENABLE_UIELEMENTS_MODULE
m_RequiresComposition = cameraData.rendersOverlayUI
&& UIElementsRuntimeUtility.AnyOverlayPanelHasBackdropFilter()
&& !cameraData.stackLastCameraOutputToHDR;
#else
m_RequiresComposition = false;
#endif
}
internal bool RequiresComposition() => m_RequiresComposition;
/// <summary>
/// Get a descriptor for the required color texture for offscreen UI pass.
/// </summary>
internal static void ConfigureOffscreenUITextureDesc(ref TextureDesc textureDesc)
{
textureDesc.format = GraphicsFormat.R8G8B8A8_SRGB;
textureDesc.depthBufferBits = 0;
textureDesc.width = Screen.width;
textureDesc.height = Screen.height;
}
/// <summary>
/// Get a descriptor for the required depth texture for this pass.
/// </summary>
/// <param name="descriptor">Camera target descriptor.</param>
/// <param name="depthStencilFormat">Depth stencil format required.</param>
/// <param name="screenWidth">The full screen width.</param>
/// <param name="screenHeight">The full screen height.</param>
/// <seealso cref="RenderTextureDescriptor"/>
private static void ConfigureDepthDescriptor(ref RenderTextureDescriptor descriptor, GraphicsFormat depthStencilFormat, int screenWidth, int screenHeight)
{
descriptor.graphicsFormat = GraphicsFormat.None;
descriptor.depthStencilFormat = depthStencilFormat;
descriptor.width = screenWidth;
descriptor.height = screenHeight;
}
private static void ExecutePass(RasterCommandBuffer commandBuffer, PassData passData, RendererList rendererList)
{
commandBuffer.DrawRendererList(rendererList);
}
// Specific to RG cases which have to go through Unsafe commands
private static void ExecutePass(UnsafeCommandBuffer commandBuffer, UnsafePassData passData, RendererList rendererList)
{
commandBuffer.DrawRendererList(rendererList);
}
public void Dispose()
{
// Materials are owned by the renderer; this pass only releases the buffers it allocates.
m_OverlayCompositeBuffer?.Release();
m_OverlayCompositeBuffer = null;
m_OverlayCompositeBufferBefore?.Release();
m_OverlayCompositeBufferBefore = null;
m_OverlayDepthStencilBuffer?.Release();
m_OverlayDepthStencilBuffer = null;
}
//RenderGraph path
private class PassData
{
internal RendererListHandle rendererList;
}
// Specific to RG cases which have to go through Unsafe commands
private class UnsafePassData
{
internal RendererListHandle rendererList;
internal TextureHandle colorTarget;
// Bound with color so stencil clipping works.
internal TextureHandle depthTarget;
// Pre-UI snapshot the composite diffs against.
internal TextureHandle bufferBefore;
}
private class ViewportBlitPassData
{
internal TextureHandle source;
internal TextureHandle destination;
internal Rect viewport;
internal Material material;
internal int passIndex;
}
// Runs before the final blit — afterwards cameraColor is the backbuffer and can't be read.
internal void BlitCameraColorToOverlayCompositeBuffer(RenderGraph renderGraph, UniversalResourceData resourceData, Rect viewport, GraphicsFormat cameraDepthAttachmentFormat)
{
ImportOverlayCompositeTextures(renderGraph, resourceData, cameraDepthAttachmentFormat);
AddViewportBlitPass(renderGraph, resourceData.cameraColor, m_OverlayCompositeBufferHandle,
viewport, sampler: null, passName: "Overlay Composite Viewport Blit");
}
void ImportOverlayCompositeTextures(RenderGraph renderGraph, UniversalResourceData resourceData, GraphicsFormat cameraDepthAttachmentFormat)
{
var bbInfo = renderGraph.GetRenderTargetInfo(resourceData.backBufferColor);
var compositeDesc = new RenderTextureDescriptor(bbInfo.width, bbInfo.height, bbInfo.format, depthBufferBits: 0)
{
msaaSamples = 1,
useMipMap = false,
autoGenerateMips = false,
};
RenderingUtils.ReAllocateHandleIfNeeded(ref m_OverlayCompositeBuffer, compositeDesc,
FilterMode.Bilinear, TextureWrapMode.Clamp, name: "_OverlayCompositeBuffer");
RenderingUtils.ReAllocateHandleIfNeeded(ref m_OverlayCompositeBufferBefore, compositeDesc,
FilterMode.Bilinear, TextureWrapMode.Clamp, name: "_OverlayCompositeBufferBefore");
// Dedicated buffer: the camera depth can be a placeholder or wrong size here.
var depthStencilDesc = new RenderTextureDescriptor(bbInfo.width, bbInfo.height, GraphicsFormat.None, cameraDepthAttachmentFormat)
{
msaaSamples = 1,
useMipMap = false,
autoGenerateMips = false,
};
RenderingUtils.ReAllocateHandleIfNeeded(ref m_OverlayDepthStencilBuffer, depthStencilDesc,
FilterMode.Point, TextureWrapMode.Clamp, name: "_OverlayDepthStencilBuffer");
// Each base camera writes only its viewport; earlier cameras' regions must survive the import.
var importParams = new ImportResourceParams
{
clearOnFirstUse = false,
discardOnLastUse = false,
};
m_OverlayCompositeBufferHandle = renderGraph.ImportTexture(m_OverlayCompositeBuffer, importParams);
m_OverlayCompositeBufferBeforeHandle = renderGraph.ImportTexture(m_OverlayCompositeBufferBefore, importParams);
m_OverlayDepthStencilBufferHandle = renderGraph.ImportTexture(m_OverlayDepthStencilBuffer, importParams);
}
// No Y-flip here: source and destination share the same orientation (unlike a final blit to screen).
internal void AddViewportBlitPass(RenderGraph renderGraph, TextureHandle source, TextureHandle destination, Rect viewport, ProfilingSampler sampler, string passName)
{
if (m_SeedBlitMaterial == null)
return;
using (var builder = renderGraph.AddRasterRenderPass<ViewportBlitPassData>(passName, out var passData, sampler))
{
passData.source = source;
passData.destination = destination;
passData.viewport = viewport;
passData.material = m_SeedBlitMaterial;
passData.passIndex = 0;
builder.UseTexture(source, AccessFlags.Read);
builder.SetRenderAttachment(destination, 0, AccessFlags.Write);
builder.SetRenderFunc(static (ViewportBlitPassData data, RasterGraphContext context) =>
{
context.cmd.SetViewport(data.viewport);
RTHandle srcRTHandle = data.source;
Vector2 scale = (srcRTHandle != null && srcRTHandle.useScaling)
? new Vector2(srcRTHandle.rtHandleProperties.rtHandleScale.x, srcRTHandle.rtHandleProperties.rtHandleScale.y)
: Vector2.one;
Vector4 scaleBias = new Vector4(scale.x, scale.y, 0f, 0f);
Blitter.BlitTexture(context.cmd, data.source, scaleBias, data.material, data.passIndex);
});
}
}
private class CompositePassData
{
internal TextureHandle source;
internal TextureHandle sourceBefore;
internal TextureHandle destination;
internal Material material;
internal int passIndex;
}
static readonly int s_BlitTextureBeforeId = Shader.PropertyToID("_BlitTexture_Before");
// Composites the OverlayCompositeBuffer onto the backbuffer: pixels matching the pre-UI snapshot
// (`sourceBefore`) are discarded (keeping FinalPost output); changed pixels write the post-UI RGB.
internal void AddUIBackdropFilterCompositePass(RenderGraph renderGraph, TextureHandle source, TextureHandle sourceBefore, TextureHandle destination, ProfilingSampler sampler, string passName)
{
if (m_UIBackdropFilterCompositeMaterial == null)
return;
using (var builder = renderGraph.AddRasterRenderPass<CompositePassData>(passName, out var passData, sampler))
{
passData.source = source;
passData.sourceBefore = sourceBefore;
passData.destination = destination;
passData.material = m_UIBackdropFilterCompositeMaterial;
passData.passIndex = k_CompositePassIndex;
builder.UseTexture(source, AccessFlags.Read);
builder.UseTexture(sourceBefore, AccessFlags.Read);
builder.SetRenderAttachment(destination, 0, AccessFlags.Write);
builder.SetRenderFunc(static (CompositePassData data, RasterGraphContext context) =>
{
// Bind the pre-UI snapshot; Blitter sets _BlitTexture from data.source.
data.material.SetTexture(s_BlitTextureBeforeId, data.sourceBefore);
Vector4 scaleBias = RenderingUtils.GetFinalBlitScaleBias(context, in data.source, in data.destination);
Blitter.BlitTexture(context.cmd, data.source, scaleBias, data.material, data.passIndex);
});
}
}
internal void RenderOffscreen(RenderGraph renderGraph, ContextContainer frameData, GraphicsFormat depthStencilFormat, TextureHandle overlayUITexture)
{
UniversalCameraData cameraData = frameData.Get<UniversalCameraData>();
RenderTextureDescriptor depthDescriptor = cameraData.cameraTargetDescriptor;
ConfigureDepthDescriptor(ref depthDescriptor, depthStencilFormat, Screen.width, Screen.height);
TextureHandle depthBuffer = UniversalRenderer.CreateRenderGraphTexture(renderGraph, depthDescriptor, "_OverlayUITexture_Depth", false);
// Render uGUI and UIToolkit overlays
using (var builder = renderGraph.AddRasterRenderPass<PassData>("Draw Screen Space UIToolkit/uGUI - Offscreen", out var passData, profilingSampler))
{
// UIToolkit/uGUI pass accept custom shaders, we need to make sure we use all global textures
builder.UseAllGlobalTextures(true);
builder.SetRenderAttachment(overlayUITexture, 0);
passData.rendererList = renderGraph.CreateUIOverlayRendererList(cameraData.camera, UISubset.UIToolkit_UGUI);
builder.UseRendererList(passData.rendererList);
builder.SetRenderAttachmentDepth(depthBuffer, AccessFlags.ReadWrite);
if (overlayUITexture.IsValid())
builder.SetGlobalTextureAfterPass(overlayUITexture, ShaderPropertyId.overlayUITexture);
builder.SetRenderFunc(static (PassData data, RasterGraphContext context) =>
{
context.cmd.ClearRenderTarget(true, true, Color.clear);
ExecutePass(context.cmd, data, data.rendererList);
});
}
// Render IMGUI overlay and software cursor in a UnsafePass
// Doing so allow us to safely cover cases when graphics commands called through onGUI() in user scripts are not supported by RenderPass API
// Besides, Vulkan backend doesn't support SetSRGWrite() in RenderPass API and we have some of them at IMGUI levels
// Note, these specific UI calls doesn't need depth buffer unlike UIToolkit/uGUI
using (var builder = renderGraph.AddUnsafePass<UnsafePassData>("Draw Screen Space IMGUI/SoftwareCursor - Offscreen", out var passData, profilingSampler))
{
passData.colorTarget = overlayUITexture;
builder.UseTexture(overlayUITexture, AccessFlags.Write);
passData.rendererList = renderGraph.CreateUIOverlayRendererList(cameraData.camera, UISubset.LowLevel);
builder.UseRendererList(passData.rendererList);
builder.SetRenderFunc(static (UnsafePassData data, UnsafeGraphContext context) =>
{
context.cmd.SetRenderTarget(data.colorTarget);
ExecutePass(context.cmd, data, data.rendererList);
});
}
}
internal void RenderOverlay(RenderGraph renderGraph, ContextContainer frameData, in TextureHandle colorBuffer, in TextureHandle depthBuffer)
{
RenderOverlayUIToolkitAndUGUI(renderGraph, frameData, in colorBuffer, in depthBuffer);
RenderOverlayIMGUI(renderGraph, frameData, in colorBuffer, in depthBuffer);
}
// Default raster overlay path: used when no panel needs backdrop-filter (or the composite path can't
// run). A stray backdrop-filter element here silently no-ops, which is intended.
internal void RenderOverlayUIToolkitAndUGUI(RenderGraph renderGraph, ContextContainer frameData, in TextureHandle colorBuffer, in TextureHandle depthBuffer)
{
var cameraData = frameData.Get<UniversalCameraData>();
using (var builder = renderGraph.AddRasterRenderPass<PassData>("Draw UIToolkit/uGUI Overlay", out var passData, profilingSampler))
{
// UIToolkit/uGUI pass accept custom shaders, we need to make sure we use all global textures
builder.UseAllGlobalTextures(true);
builder.SetRenderAttachment(colorBuffer, 0);
if (depthBuffer.IsValid())
builder.SetRenderAttachmentDepth(depthBuffer, AccessFlags.ReadWrite);
passData.rendererList = renderGraph.CreateUIOverlayRendererList(cameraData.camera, UISubset.UIToolkit_UGUI);
builder.UseRendererList(passData.rendererList);
builder.SetRenderFunc(static (PassData data, RasterGraphContext context) =>
{
ExecutePass(context.cmd, data, data.rendererList);
});
}
}
// Composite variant: used when an overlay panel has backdrop-filter. UIToolkit renders into
// m_OverlayCompositeBuffer (pre-filled with the scene); a final diff-composite writes it to the backbuffer.
internal void RenderOverlayUIToolkitAndUGUIComposite(RenderGraph renderGraph, ContextContainer frameData, in TextureHandle destinationColor)
{
var cameraData = frameData.Get<UniversalCameraData>();
// Pass 1 — render UIToolkit/uGUI into the composite buffer (backdrop-filter samples it as it fills).
using (var builder = renderGraph.AddUnsafePass<UnsafePassData>("Draw UIToolkit/uGUI Overlay (Backdrop Composite)", out var passData, profilingSampler))
{
// UIToolkit/uGUI pass accepts custom shaders; we need to make sure we use all global textures
builder.UseAllGlobalTextures(true);
passData.colorTarget = m_OverlayCompositeBufferHandle;
passData.depthTarget = m_OverlayDepthStencilBufferHandle;
passData.bufferBefore = m_OverlayCompositeBufferBeforeHandle;
builder.UseTexture(m_OverlayCompositeBufferHandle, AccessFlags.ReadWrite);
builder.UseTexture(m_OverlayDepthStencilBufferHandle, AccessFlags.ReadWrite);
if (m_OverlayCompositeBufferBeforeHandle.IsValid())
builder.UseTexture(m_OverlayCompositeBufferBeforeHandle, AccessFlags.Write);
passData.rendererList = renderGraph.CreateUIOverlayRendererList(cameraData.camera, UISubset.UIToolkit_UGUI);
builder.UseRendererList(passData.rendererList);
builder.SetRenderFunc(static (UnsafePassData data, UnsafeGraphContext context) =>
{
// Copy the pre-UI buffer with CopyTexture (an exact copy; a shader blit would shift
// values slightly and break the diff).
if (data.bufferBefore.IsValid())
{
RTHandle src = data.colorTarget;
RTHandle dst = data.bufferBefore;
if (src != null && dst != null)
context.cmd.CopyTexture(src.nameID, dst.nameID);
}
// Bind the composite buffer as active RT: UIToolkit's BackdropFilterHelper samples
// RenderTexture.active at submission, so it must be set before DrawRendererList.
context.cmd.SetRenderTarget(data.colorTarget, data.depthTarget);
// Clear depth/stencil only (keep the seeded color): a stale stencil would drop the UI.
context.cmd.ClearRenderTarget(RTClearFlags.DepthStencil, Color.clear, 1.0f, 0);
ExecutePass(context.cmd, data, data.rendererList);
});
}
// Pass 2 — diff-composite the buffer over the backbuffer (non-UI pixels keep their FinalPost output).
AddUIBackdropFilterCompositePass(renderGraph, m_OverlayCompositeBufferHandle, m_OverlayCompositeBufferBeforeHandle, destinationColor, profilingSampler, "Overlay Composite To Backbuffer");
}
internal void RenderOverlayIMGUI(RenderGraph renderGraph, ContextContainer frameData, in TextureHandle colorBuffer, in TextureHandle depthBuffer)
{
var cameraData = frameData.Get<UniversalCameraData>();
// Render IMGUI overlay and software cursor in a UnsafePass
// Doing so allow us to safely cover cases when graphics commands called through onGUI() in user scripts are not supported by RenderPass API
// Besides, Vulkan backend doesn't support SetSRGWrite() in RenderPass API and we have some of them at IMGUI levels
// Note, these specific UI calls doesn't need depth buffer unlike UIToolkit/uGUI
using (var builder = renderGraph.AddUnsafePass<UnsafePassData>("Draw IMGUI/SoftwareCursor Overlay", out var passData, profilingSampler))
{
passData.colorTarget = colorBuffer;
builder.UseTexture(colorBuffer, AccessFlags.Write);
passData.rendererList = renderGraph.CreateUIOverlayRendererList(cameraData.camera, UISubset.LowLevel);
builder.UseRendererList(passData.rendererList);
builder.SetRenderFunc(static (UnsafePassData data, UnsafeGraphContext context) =>
{
context.cmd.SetRenderTarget(data.colorTarget);
ExecutePass(context.cmd, data, data.rendererList);
});
}
}
}
}