You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

245 lines
11 KiB

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UnityEngine.Rendering.RenderGraphModule;
using UnityEngine.Rendering.RenderGraphModule.Util;
/// <summary>
/// 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).
/// </summary>
public class TiltShiftFeature : ScriptableRendererFeature
{
public enum BlurMode
{
/// <summary>Blur based on vertical UV position. No depth buffer required.</summary>
ScreenSpace,
/// <summary>
/// Blur based on distance from a tilted focal plane in world space (Scheimpflug principle).
/// Requires the camera's Depth Texture to be enabled.
/// </summary>
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<UniversalResourceData>();
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<PassData>(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);
});
}
}
}