using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.RenderGraphModule; using UnityEngine.Rendering.Universal; /// /// Tilt-shift post-process effect for URP 17 (Unity 6). /// /// Two modes: /// ScreenSpace — blur based on vertical screen position (fast, no depth buffer needed). /// DepthBased — Scheimpflug tilt-plane CoC using the depth buffer (physically accurate). /// public class TiltShiftFeature : ScriptableRendererFeature { public enum BlurMode { /// Blur based on vertical UV position. No depth buffer required. ScreenSpace, /// /// Blur based on distance from a tilted focal plane in world space (Scheimpflug principle). /// Requires the camera Depth Texture to be enabled. /// DepthBased } [System.Serializable] public class Settings { [Tooltip("ScreenSpace: blur by vertical screen position.\n" + "DepthBased: physically-accurate Scheimpflug tilt-plane CoC (needs Depth Texture).")] public BlurMode blurMode = BlurMode.ScreenSpace; [Header("Screen-Space Mode")] [Tooltip("Vertical offset of the sharp band from screen centre (UV units, + = up).")] [Range(-0.4f, 0.4f)] public float centerOffset = 0f; [Tooltip("Height of the fully-sharp band as a fraction of screen height.")] [Range(0.01f, 0.8f)] public float focusWidth = 0.2f; [Tooltip("Distance over which blur fades in at each edge of the focus band (UV units).")] [Range(0.01f, 0.4f)] public float falloffRange = 0.15f; [Header("Depth-Based Mode (Scheimpflug)")] [Tooltip("Depth of the focal plane centre (world units). " + "Increase until the subject you want sharp is in focus.")] [Range(0.1f, 1000f)] public float focusDistance = 10f; [Tooltip("How much the focal plane depth shifts per screen-height unit.\n" + "Positive = top of screen focuses deeper (typical for bird's-eye view).\n" + "Set to 0 for standard (flat) DoF.")] [Range(-200f, 200f)] public float tiltFactor = 30f; [Tooltip("Half-width of the perfectly sharp zone around the focal plane (world units).")] [Range(0f, 50f)] public float focusBand = 1f; [Tooltip("World-unit distance over which blur ramps from zero to maximum.")] [Range(0.1f, 100f)] public float depthFalloff = 5f; [Header("Shared")] [Tooltip("Maximum blur radius in pixels at the extremes.")] [Range(1f, 64f)] public float maxBlurRadius = 20f; [Tooltip("Gaussian samples per side per pass. Higher = smoother, slower.")] [Range(2, 20)] public int sampleCount = 8; public RenderPassEvent injectionPoint = RenderPassEvent.AfterRenderingPostProcessing; } public Settings settings = new Settings(); private TiltShiftPass renderPass; private Material blitMaterial; // ── Lifecycle ───────────────────────────────────────────────────────────────── public override void Create() { var shader = Shader.Find("Custom/TiltShift"); if (shader == null) { Debug.LogWarning("[TiltShift] Shader 'Custom/TiltShift' not found."); return; } blitMaterial = CoreUtils.CreateEngineMaterial(shader); renderPass = new TiltShiftPass(blitMaterial, settings); renderPass.renderPassEvent = settings.injectionPoint; } public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { if (blitMaterial == null || renderPass == null) { return; } var camType = renderingData.cameraData.cameraType; if (camType == CameraType.Preview || camType == CameraType.Reflection) { return; } renderPass.UpdateSettings(settings); renderPass.renderPassEvent = settings.injectionPoint; renderer.EnqueuePass(renderPass); } protected override void Dispose(bool disposing) { CoreUtils.Destroy(blitMaterial); } // ── Inner render pass ────────────────────────────────────────────────────────── private sealed class TiltShiftPass : ScriptableRenderPass { // Shader property IDs private static readonly int propMaxBlurRadius = Shader.PropertyToID("_TiltShift_MaxBlurRadius"); private static readonly int propSampleCount = Shader.PropertyToID("_TiltShift_SampleCount"); private static readonly int propCenterOffset = Shader.PropertyToID("_TiltShift_CenterOffset"); private static readonly int propFocusWidth = Shader.PropertyToID("_TiltShift_FocusWidth"); private static readonly int propFalloffRange = Shader.PropertyToID("_TiltShift_FalloffRange"); private static readonly int propFocusDistance = Shader.PropertyToID("_TiltShift_FocusDistance"); private static readonly int propTiltFactor = Shader.PropertyToID("_TiltShift_TiltFactor"); private static readonly int propFocusBand = Shader.PropertyToID("_TiltShift_FocusBand"); private static readonly int propDepthFalloff = Shader.PropertyToID("_TiltShift_DepthFalloff"); private const string depthModeKeyword = "DEPTH_MODE"; private readonly Material blitMaterial; private Settings currentSettings; // PassData carries what the render func needs at execution time. private class PassData { public TextureHandle source; public Material material; public int passIndex; } public TiltShiftPass(Material mat, Settings passSettings) { blitMaterial = mat; currentSettings = passSettings; requiresIntermediateTexture = true; } public void UpdateSettings(Settings s) { currentSettings = s; } // ── RecordRenderGraph ────────────────────────────────────────────────────── public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData) { var resourceData = frameData.Get(); if (resourceData.isActiveTargetBackBuffer) { return; } var useDepth = currentSettings.blurMode == BlurMode.DepthBased; // Validate depth texture availability. if (useDepth && !resourceData.cameraDepthTexture.IsValid()) { Debug.LogWarning("[TiltShift] DepthBased mode requires the camera Depth Texture. " + "Enable it in the URP Renderer asset or Camera component."); useDepth = false; } // Push settings to material. blitMaterial.SetFloat(propMaxBlurRadius, currentSettings.maxBlurRadius); blitMaterial.SetInt(propSampleCount, currentSettings.sampleCount); blitMaterial.SetFloat(propCenterOffset, currentSettings.centerOffset); blitMaterial.SetFloat(propFocusWidth, currentSettings.focusWidth); blitMaterial.SetFloat(propFalloffRange, currentSettings.falloffRange); blitMaterial.SetFloat(propFocusDistance, currentSettings.focusDistance); blitMaterial.SetFloat(propTiltFactor, currentSettings.tiltFactor); blitMaterial.SetFloat(propFocusBand, currentSettings.focusBand); blitMaterial.SetFloat(propDepthFalloff, currentSettings.depthFalloff); if (useDepth) { blitMaterial.EnableKeyword(depthModeKeyword); } else { blitMaterial.DisableKeyword(depthModeKeyword); } // Texture handles var source = resourceData.activeColorTexture; var depth = useDepth ? resourceData.cameraDepthTexture : TextureHandle.nullHandle; var desc = renderGraph.GetTextureDesc(source); desc.clearBuffer = false; desc.name = "TiltShift_Temp"; var temp = renderGraph.CreateTexture(desc); desc.name = "TiltShift_Final"; var final = renderGraph.CreateTexture(desc); // Pass 0 — horizontal blur (source to temp) AddBlurPass(renderGraph, "TiltShift_H", source, depth, temp, passIndex: 0, useDepth); // Pass 1 — vertical blur (temp to final) AddBlurPass(renderGraph, "TiltShift_V", temp, depth, final, passIndex: 1, useDepth); // Redirect camera colour so subsequent passes use our result. resourceData.cameraColor = final; } // ── Helper: record one blur pass ─────────────────────────────────────────── private void AddBlurPass( RenderGraph renderGraph, string passName, TextureHandle source, TextureHandle depth, TextureHandle dest, int passIndex, bool useDepth) { using var builder = renderGraph.AddRasterRenderPass(passName, out var passData); passData.source = source; passData.material = blitMaterial; passData.passIndex = passIndex; builder.UseTexture(source, AccessFlags.Read); // Declare the depth dependency so the render graph orders this pass after CopyDepthPass. // We do not call SetGlobalTexture here — URP's CopyDepthPass already exposes // _CameraDepthTexture globally via SetGlobalTextureAfterPass. if (useDepth && depth.IsValid()) { builder.UseTexture(depth, AccessFlags.Read); } builder.SetRenderAttachment(dest, 0, AccessFlags.Write); builder.SetRenderFunc((PassData data, RasterGraphContext ctx) => { Blitter.BlitTexture(ctx.cmd, data.source, new Vector4(1, 1, 0, 0), data.material, data.passIndex); }); } } }