using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; using UnityEngine.Rendering.RenderGraphModule; using UnityEngine.Rendering.RenderGraphModule.Util; /// /// Tilt-shift (移軸鏡) post-process effect for URP 17 (Unity 6). /// /// Two modes: /// • ScreenSpace — blur based on vertical screen position (fast, camera-angle dependent). /// • 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's 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(); TiltShiftPass _pass; Material _material; // ── Lifecycle ────────────────────────────────────────────────────────────────── public override void Create() { var shader = Shader.Find("Custom/TiltShift"); if (shader == null) { Debug.LogWarning("[TiltShift] Shader 'Custom/TiltShift' not found."); return; } _material = CoreUtils.CreateEngineMaterial(shader); _pass = new TiltShiftPass(_material, settings); _pass.renderPassEvent = settings.injectionPoint; } public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { if (_material == null || _pass == null) return; var camType = renderingData.cameraData.cameraType; if (camType == CameraType.Preview || camType == CameraType.Reflection) return; _pass.UpdateSettings(settings); _pass.renderPassEvent = settings.injectionPoint; renderer.EnqueuePass(_pass); } protected override void Dispose(bool disposing) { CoreUtils.Destroy(_material); } // ── Inner render pass ────────────────────────────────────────────────────────── sealed class TiltShiftPass : ScriptableRenderPass { // Shader property IDs static readonly int s_MaxBlurRadius = Shader.PropertyToID("_TiltShift_MaxBlurRadius"); static readonly int s_SampleCount = Shader.PropertyToID("_TiltShift_SampleCount"); static readonly int s_CenterOffset = Shader.PropertyToID("_TiltShift_CenterOffset"); static readonly int s_FocusWidth = Shader.PropertyToID("_TiltShift_FocusWidth"); static readonly int s_FalloffRange = Shader.PropertyToID("_TiltShift_FalloffRange"); static readonly int s_FocusDistance = Shader.PropertyToID("_TiltShift_FocusDistance"); static readonly int s_TiltFactor = Shader.PropertyToID("_TiltShift_TiltFactor"); static readonly int s_FocusBand = Shader.PropertyToID("_TiltShift_FocusBand"); static readonly int s_DepthFalloff = Shader.PropertyToID("_TiltShift_DepthFalloff"); // Shader keyword for mode switch const string k_DepthModeKeyword = "DEPTH_MODE"; readonly Material _material; Settings _settings; // PassData carries what the render func needs at execution time. class PassData { public TextureHandle source; public Material material; public int passIndex; } public TiltShiftPass(Material material, Settings settings) { _material = material; _settings = settings; requiresIntermediateTexture = true; } public void UpdateSettings(Settings s) => _settings = s; // ── RecordRenderGraph ────────────────────────────────────────────────────── public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData) { var resourceData = frameData.Get(); if (resourceData.isActiveTargetBackBuffer) return; bool useDepth = _settings.blurMode == BlurMode.DepthBased; // Validate depth texture availability. if (useDepth && !resourceData.cameraDepthTexture.IsValid()) { Debug.LogWarning("[TiltShift] DepthBased mode requires the camera's Depth Texture. " + "Enable it in the URP Renderer asset or Camera component."); useDepth = false; } // ── Push settings to material ────────────────────────────────────────── _material.SetFloat(s_MaxBlurRadius, _settings.maxBlurRadius); _material.SetInt (s_SampleCount, _settings.sampleCount); _material.SetFloat(s_CenterOffset, _settings.centerOffset); _material.SetFloat(s_FocusWidth, _settings.focusWidth); _material.SetFloat(s_FalloffRange, _settings.falloffRange); _material.SetFloat(s_FocusDistance, _settings.focusDistance); _material.SetFloat(s_TiltFactor, _settings.tiltFactor); _material.SetFloat(s_FocusBand, _settings.focusBand); _material.SetFloat(s_DepthFalloff, _settings.depthFalloff); if (useDepth) _material.EnableKeyword(k_DepthModeKeyword); else _material.DisableKeyword(k_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"; TextureHandle temp = renderGraph.CreateTexture(desc); desc.name = "TiltShift_Final"; TextureHandle final = renderGraph.CreateTexture(desc); // ── Pass 0 — Horizontal blur (source → temp) ────────────────────────── AddBlurPass(renderGraph, "TiltShift_H", source, depth, temp, passIndex: 0, useDepth); // ── Pass 1 — Vertical blur (temp → final) ──────────────────────────── AddBlurPass(renderGraph, "TiltShift_V", temp, depth, final, passIndex: 1, useDepth); // Redirect camera colour so subsequent passes use our result. resourceData.cameraColor = final; } // ── Helper to record one blur pass ───────────────────────────────────────── 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 = _material; passData.passIndex = passIndex; builder.UseTexture(source, AccessFlags.Read); // Declare the dependency on the depth texture so the render graph correctly // orders this pass after URP's CopyDepthPass. We do NOT call SetGlobalTexture // here — URP's CopyDepthPass already exposes _CameraDepthTexture globally via // SetGlobalTextureAfterPass, so the shader can sample it without any extra work. 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); }); } } }