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

293 lines
14 KiB
C#

using System;
using UnityEngine.Experimental.Rendering;
using UnityEngine.Rendering.RenderGraphModule;
namespace UnityEngine.Rendering.Universal
{
[Serializable]
internal class ScreenSpaceShadowsSettings
{
}
[SupportedOnRenderer(typeof(UniversalRendererData))]
[DisallowMultipleRendererFeature("Screen Space Shadows")]
[Tooltip("Screen Space Shadows")]
[URPHelpURL("urp/renderer-feature-screen-space-shadows")]
internal class ScreenSpaceShadows : ScriptableRendererFeature
{
#if UNITY_EDITOR
[UnityEditor.ShaderKeywordFilter.SelectIf(true, keywordNames: ShaderKeywordStrings.MainLightShadowScreen)]
private const bool k_RequiresScreenSpaceShadowsKeyword = true;
#endif
// Serialized Fields
[SerializeField, HideInInspector] private Shader m_Shader = null;
[SerializeField] private ScreenSpaceShadowsSettings m_Settings = new ScreenSpaceShadowsSettings();
// Private Fields
private Material m_Material;
private ScreenSpaceShadowsPass m_SSShadowsPass = null;
private ScreenSpaceShadowsPostPass m_SSShadowsPostPass = null;
// Constants
private const string k_ShaderName = "Hidden/Universal Render Pipeline/ScreenSpaceShadows";
/// <inheritdoc/>
public override void Create()
{
if (m_SSShadowsPass == null)
m_SSShadowsPass = new ScreenSpaceShadowsPass();
if (m_SSShadowsPostPass == null)
m_SSShadowsPostPass = new ScreenSpaceShadowsPostPass();
LoadMaterial();
m_SSShadowsPass.renderPassEvent = RenderPassEvent.BeforeRenderingGbuffer;
m_SSShadowsPostPass.renderPassEvent = RenderPassEvent.BeforeRenderingTransparents;
}
/// <inheritdoc/>
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
if (UniversalRenderer.IsOffscreenDepthTexture(ref renderingData.cameraData))
return;
if (!LoadMaterial())
{
Debug.LogErrorFormat(
"{0}.AddRenderPasses(): Missing material. {1} render pass will not be added. Check for missing reference in the renderer resources.",
GetType().Name, name);
return;
}
bool allowMainLightShadows = renderingData.shadowData.supportsMainLightShadows && renderingData.lightData.mainLightIndex != -1;
bool shouldEnqueue = allowMainLightShadows && m_SSShadowsPass.Setup(m_Settings, m_Material);
if (shouldEnqueue)
{
bool usesDeferredLighting = renderer is UniversalRenderer { usesDeferredLighting: true };
m_SSShadowsPass.renderPassEvent = usesDeferredLighting
? RenderPassEvent.BeforeRenderingGbuffer
: RenderPassEvent.AfterRenderingPrePasses + 1; // We add 1 to ensure this happens after depth priming depth copy pass that might be scheduled
renderer.EnqueuePass(m_SSShadowsPass);
renderer.EnqueuePass(m_SSShadowsPostPass);
}
}
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
m_SSShadowsPass = null;
CoreUtils.Destroy(m_Material);
}
private bool LoadMaterial()
{
if (m_Material != null)
{
return true;
}
if (m_Shader == null)
{
m_Shader = Shader.Find(k_ShaderName);
if (m_Shader == null)
{
return false;
}
}
m_Material = CoreUtils.CreateEngineMaterial(m_Shader);
return m_Material != null;
}
/// <summary>
/// Pass that renders screen-space shadows by sampling the main light shadow map.
///
/// This pass reconstructs world positions from the depth prepass (cameraDepthTexture),
/// transforms them to shadow space, and samples the shadow map to produce a screen-space
/// shadow texture. This is more efficient than traditional forward shadow sampling per-pixel.
///
/// IMPORTANT: World position reconstruction from depth requires the inverse view-projection matrix
/// (unity_MatrixInvVP) to match the UV origin of the depth texture. In Tile-Only Mode and
/// direct-to-backbuffer scenarios, the depth texture orientation may differ from the active
/// render target, requiring explicit matrix setup. See ExecutePass for details.
/// </summary>
private class ScreenSpaceShadowsPass : ScriptableRenderPass
{
// Private Variables
private Material m_Material;
private ScreenSpaceShadowsSettings m_CurrentSettings;
private int m_ScreenSpaceShadowmapTextureID;
internal ScreenSpaceShadowsPass()
{
profilingSampler = new ProfilingSampler("Blit Screen Space Shadows");
m_CurrentSettings = new ScreenSpaceShadowsSettings();
m_ScreenSpaceShadowmapTextureID = Shader.PropertyToID("_ScreenSpaceShadowmapTexture");
}
internal bool Setup(ScreenSpaceShadowsSettings featureSettings, Material material)
{
m_CurrentSettings = featureSettings;
m_Material = material;
ConfigureInput(ScriptableRenderPassInput.Depth);
return m_Material != null;
}
private class PassData
{
internal TextureHandle target;
internal TextureHandle cameraDepthTexture;
internal TextureHandle activeTarget;
internal Material material;
internal UniversalCameraData cameraData;
}
/// <summary>
/// Initialize the shared pass data.
/// </summary>
/// <param name="passData"></param>
private void InitPassData(ref PassData passData, in TextureHandle cameraDepthTexture, in TextureHandle activeTarget, UniversalCameraData cameraData)
{
passData.material = m_Material;
passData.cameraDepthTexture = cameraDepthTexture;
passData.activeTarget = activeTarget;
passData.cameraData = cameraData;
}
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
if (m_Material == null)
{
Debug.LogErrorFormat("{0}.Execute(): Missing material. ScreenSpaceShadows pass will not execute. Check for missing reference in the renderer resources.", GetType().Name);
return;
}
var cameraData = frameData.Get<UniversalCameraData>();
var resourceData = frameData.Get<UniversalResourceData>();
var desc = cameraData.cameraTargetDescriptor;
desc.depthStencilFormat = GraphicsFormat.None;
desc.msaaSamples = 1;
// UUM-41070: We require `Linear | Render` but with the deprecated FormatUsage this was checking `Blend`
// For now, we keep checking for `Blend` until the performance hit of doing the correct checks is evaluated
desc.graphicsFormat = SystemInfo.IsFormatSupported(GraphicsFormat.R8_UNorm, GraphicsFormatUsage.Blend)
? GraphicsFormat.R8_UNorm
: GraphicsFormat.B8G8R8A8_UNorm;
TextureHandle color = UniversalRenderer.CreateRenderGraphTexture(renderGraph, desc, "_ScreenSpaceShadowmapTexture", true);
// UUM-85291: Using UnsafePass to not allow this pass to merge with other passes as it can cause issues
// when using Deferred Lighting by breaking up the Draw GBuffer and Deferred Lighting passes because
// of 1) the Deferred Lighting pass reads this resource so it breaks the pass 2) a maximum input attachment
// limit is met when this is moved before Draw GBuffer.
// For now, using an UnsafePass ensures that this pass won't be merged as a fix is found for the other
// underlying issues.
using (var builder = renderGraph.AddUnsafePass<PassData>(passName, out var passData, profilingSampler))
{
passData.target = color;
builder.UseTexture(color, AccessFlags.WriteAll);
builder.UseTexture(resourceData.cameraDepthTexture);
// activeColorTexture is always valid here since AddRenderPasses returns early for offscreen depth cameras
InitPassData(ref passData, resourceData.cameraDepthTexture, resourceData.activeColorTexture, cameraData);
builder.AllowGlobalStateModification(true);
if (color.IsValid())
builder.SetGlobalTextureAfterPass(color, m_ScreenSpaceShadowmapTextureID);
builder.SetRenderFunc(static (PassData data, UnsafeGraphContext rgContext) =>
{
ExecutePass(rgContext, data);
});
}
}
private static void ExecutePass(UnsafeGraphContext rgContext, PassData data)
{
var cmd = rgContext.cmd;
// CRITICAL FIX for Tile-Only Mode and direct-to-backbuffer rendering:
//
// The shader reconstructs world positions from depth using:
// float3 wpos = ComputeWorldSpacePosition(uv, depth, unity_MatrixInvVP);
//
// The global unity_MatrixInvVP might be set for the active render target's orientation (e.g., TopLeft in Tile-Only Mode),
// but cameraDepthTexture was rendered with a potentially different orientation (always BottomLeft - it's an intermediate texture).
//
// When orientations don't match, the Y-coordinate in world-space reconstruction is inverted, causing shadow lookups
// to sample from the wrong world positions → shadows appear upside down.
//
// FIX: Temporarily set unity_MatrixInvVP to match cameraDepthTexture's orientation for correct position reconstruction.
TextureUVOrigin depthOrigin = rgContext.GetTextureUVOrigin(data.cameraDepthTexture);
Matrix4x4 depthInvVP = RenderingUtils.ComputeInverseViewProjectionMatrix(depthOrigin, data.cameraData);
cmd.SetGlobalMatrix(ShaderPropertyId.inverseViewAndProjectionMatrix, depthInvVP);
// Perform the shadow sampling blit
Blitter.BlitTexture(cmd, data.target, Vector2.one, data.material, 0);
// Set shadow keywords
cmd.SetKeyword(ShaderGlobalKeywords.MainLightShadows, false);
cmd.SetKeyword(ShaderGlobalKeywords.MainLightShadowCascades, false);
cmd.SetKeyword(ShaderGlobalKeywords.MainLightShadowScreen, true);
// Restore unity_MatrixInvVP to match the active target's orientation for subsequent passes.
// Without this, later passes that rely on the global matrix might get incorrect results.
TextureUVOrigin activeOrigin = rgContext.GetTextureUVOrigin(data.activeTarget);
Matrix4x4 activeInvVP = RenderingUtils.ComputeInverseViewProjectionMatrix(activeOrigin, data.cameraData);
cmd.SetGlobalMatrix(ShaderPropertyId.inverseViewAndProjectionMatrix, activeInvVP);
}
}
private class ScreenSpaceShadowsPostPass : ScriptableRenderPass
{
internal ScreenSpaceShadowsPostPass()
{
profilingSampler = new ProfilingSampler("Set Screen Space Shadow Keywords");
}
private static void ExecutePass(RasterCommandBuffer cmd, UniversalShadowData shadowData)
{
int cascadesCount = shadowData.mainLightShadowCascadesCount;
bool mainLightShadows = shadowData.supportsMainLightShadows;
bool receiveShadowsNoCascade = mainLightShadows && cascadesCount == 1;
bool receiveShadowsCascades = mainLightShadows && cascadesCount > 1;
// Before transparent object pass, force to disable screen space shadow of main light
cmd.SetKeyword(ShaderGlobalKeywords.MainLightShadowScreen, false);
// then enable main light shadows with or without cascades
cmd.SetKeyword(ShaderGlobalKeywords.MainLightShadows, receiveShadowsNoCascade);
cmd.SetKeyword(ShaderGlobalKeywords.MainLightShadowCascades, receiveShadowsCascades);
}
internal class PassData
{
internal UniversalShadowData shadowData;
}
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
using (var builder = renderGraph.AddRasterRenderPass<PassData>(passName, out var passData, profilingSampler))
{
UniversalResourceData resourceData = frameData.Get<UniversalResourceData>();
TextureHandle color = resourceData.activeColorTexture;
builder.SetRenderAttachment(color, 0, AccessFlags.Write);
passData.shadowData = frameData.Get<UniversalShadowData>();
builder.AllowGlobalStateModification(true);
builder.SetRenderFunc(static (PassData data, RasterGraphContext rgContext) =>
{
ExecutePass(rgContext.cmd, data.shadowData);
});
}
}
}
}
}