using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Experimental.Rendering;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UnityEngine.Rendering.RenderGraphModule;
namespace MS_Outline
{
///
/// URP Renderer Feature that creates a screen-space glow effect around highlighted objects.
/// Add this to your URP Renderer asset.
///
public class GlowOutlineRendererFeature : ScriptableRendererFeature
{
[System.Serializable]
public class FullOutlineSettings
{
[Header("Render Settings")]
[Tooltip("When to inject the outline pass")]
public RenderPassEvent renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;
[Tooltip("Downscale factor for blur (1 = full res, 2 = half, 4 = quarter)")]
[Range(1, 4)]
public int downsample = 2;
[Header("Blur Settings")]
[Tooltip("Number of blur iterations (more = softer glow, higher cost)")]
[Range(1, 8)]
public int blurIterations = 2;
[Tooltip("Base blur spread")]
[Range(0.5f, 3f)]
public float blurSpread = 0.5f;
[Header("Inner Edge Settings")]
[Tooltip("Erode silhouette to hide aliased inner edge (pixels). 0 = no erosion.")]
[Range(0, 8)]
public int innerEdgeErosion = 5;
[Header("Occlusion Settings")]
[Tooltip("Number of depth dilation iterations (more = wider occlusion coverage)")]
[Range(1, 16)]
public int depthDilationIterations = 7;
[Tooltip("Dilation spread per iteration")]
[Range(0.5f, 3f)]
public float dilationSpread = 1.03f;
[Tooltip("Minimum depth bias for occlusion comparison (prevents z-fighting at close range)")]
[Range(0f, 0.01f)]
public float occlusionBiasMin = 0.000013f;
[Tooltip("Depth bias scale - multiplied by object depth (handles varying distances)")]
[Range(0f, 0.1f)]
public float occlusionBiasScale = 0.0305f;
[Header("Glow Settings")]
[Tooltip("Overall glow intensity multiplier")]
[Range(0f, 3f)]
public float glowIntensity = 1.27f;
[Header("Debug")]
[Tooltip("Show debug visualization instead of final output")]
public DebugMode debugMode = DebugMode.None;
[Header("Shaders")]
public Shader silhouetteShader;
public Shader blurShader;
public Shader compositeShader;
public Shader depthDilateShader;
public Shader erodeShader;
}
public enum DebugMode
{
None,
Silhouette,
ErodedSilhouette,
ObjectDepth,
DilatedDepth,
OcclusionMask,
BlurredGlow
}
public FullOutlineSettings settings = new FullOutlineSettings();
private FullOutlinePass outlinePass;
private Material silhouetteMaterial;
private Material blurMaterial;
private Material compositeMaterial;
private Material depthDilateMaterial;
private Material erodeMaterial;
public override void Create()
{
// Load shaders if not assigned
if (settings.silhouetteShader == null)
{
settings.silhouetteShader = Shader.Find("MS_RPG/FullOutline/Silhouette");
if (settings.silhouetteShader == null)
Debug.LogError("[FullOutline] Could not find Silhouette shader!");
}
if (settings.blurShader == null)
{
settings.blurShader = Shader.Find("MS_RPG/FullOutline/KawaseBlur");
if (settings.blurShader == null)
Debug.LogError("[FullOutline] Could not find KawaseBlur shader!");
}
if (settings.compositeShader == null)
{
settings.compositeShader = Shader.Find("MS_RPG/FullOutline/Composite");
if (settings.compositeShader == null)
Debug.LogError("[FullOutline] Could not find Composite shader!");
}
if (settings.depthDilateShader == null)
{
settings.depthDilateShader = Shader.Find("MS_RPG/FullOutline/DepthDilate");
if (settings.depthDilateShader == null)
Debug.LogError("[FullOutline] Could not find DepthDilate shader!");
}
if (settings.erodeShader == null)
{
settings.erodeShader = Shader.Find("MS_RPG/FullOutline/Erode");
if (settings.erodeShader == null)
Debug.LogError("[FullOutline] Could not find Erode shader!");
}
// Create materials
if (settings.silhouetteShader != null)
silhouetteMaterial = CoreUtils.CreateEngineMaterial(settings.silhouetteShader);
if (settings.blurShader != null)
blurMaterial = CoreUtils.CreateEngineMaterial(settings.blurShader);
if (settings.compositeShader != null)
compositeMaterial = CoreUtils.CreateEngineMaterial(settings.compositeShader);
if (settings.depthDilateShader != null)
depthDilateMaterial = CoreUtils.CreateEngineMaterial(settings.depthDilateShader);
if (settings.erodeShader != null)
erodeMaterial = CoreUtils.CreateEngineMaterial(settings.erodeShader);
outlinePass = new FullOutlinePass(settings, silhouetteMaterial, blurMaterial, compositeMaterial, depthDilateMaterial, erodeMaterial);
outlinePass.renderPassEvent = settings.renderPassEvent;
}
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
// Only render if we have active targets
if (GlowOutlineTarget.ActiveTargets.Count == 0)
return;
// Don't render in scene view previews or reflection probes
if (renderingData.cameraData.cameraType != CameraType.Game &&
renderingData.cameraData.cameraType != CameraType.SceneView)
return;
if (outlinePass != null && outlinePass.IsValid())
{
renderer.EnqueuePass(outlinePass);
}
}
protected override void Dispose(bool disposing)
{
CoreUtils.Destroy(silhouetteMaterial);
CoreUtils.Destroy(blurMaterial);
CoreUtils.Destroy(compositeMaterial);
CoreUtils.Destroy(depthDilateMaterial);
CoreUtils.Destroy(erodeMaterial);
}
}
///
/// The actual render pass that performs silhouette rendering, blur, and compositing
///
public class FullOutlinePass : ScriptableRenderPass
{
private GlowOutlineRendererFeature.FullOutlineSettings settings;
private Material silhouetteMaterial;
private Material blurMaterial;
private Material compositeMaterial;
private Material depthDilateMaterial;
private Material erodeMaterial;
// Property IDs
private static readonly int ColorId = Shader.PropertyToID("_Color");
private static readonly int IntensityId = Shader.PropertyToID("_Intensity");
private static readonly int OffsetId = Shader.PropertyToID("_Offset");
private static readonly int GlowIntensityId = Shader.PropertyToID("_GlowIntensity");
private static readonly int OcclusionBiasMinId = Shader.PropertyToID("_OcclusionBiasMin");
private static readonly int OcclusionBiasScaleId = Shader.PropertyToID("_OcclusionBiasScale");
public FullOutlinePass(GlowOutlineRendererFeature.FullOutlineSettings settings,
Material silhouetteMaterial, Material blurMaterial, Material compositeMaterial, Material depthDilateMaterial, Material erodeMaterial)
{
this.settings = settings;
this.silhouetteMaterial = silhouetteMaterial;
this.blurMaterial = blurMaterial;
this.compositeMaterial = compositeMaterial;
this.depthDilateMaterial = depthDilateMaterial;
this.erodeMaterial = erodeMaterial;
this.profilingSampler = new ProfilingSampler("FullOutline");
}
public bool IsValid()
{
return silhouetteMaterial != null && blurMaterial != null && compositeMaterial != null && depthDilateMaterial != null;
}
// PassData for silhouette rendering
private class SilhouettePassData
{
public Material silhouetteMaterial;
public TextureHandle silhouetteTexture;
}
// PassData for object depth rendering
private class ObjectDepthPassData
{
public Material silhouetteMaterial;
public TextureHandle depthTexture;
}
// PassData for blit operations
private class BlitPassData
{
public Material material;
public TextureHandle source;
public TextureHandle destination;
public float offset;
public int passIndex;
}
// PassData for occlusion comparison
private class OcclusionComparePassData
{
public Material material;
public TextureHandle source;
public TextureHandle destination;
public float biasMin;
public float biasScale;
}
// PassData for composite
private class CompositePassData
{
public Material compositeMaterial;
public TextureHandle sceneColor;
public TextureHandle glowTexture;
public TextureHandle occlusionTexture;
public TextureHandle silhouetteTexture;
public TextureHandle erodedSilhouetteTexture; // Eroded silhouette for subtraction (hides aliased inner edge)
public TextureHandle objectDepthTexture;
public TextureHandle dilatedDepthTexture;
public TextureHandle tempTexture;
public float glowIntensity;
public float occlusionBiasMin;
public float occlusionBiasScale;
public GlowOutlineRendererFeature.DebugMode debugMode;
}
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
if (!IsValid())
return;
var activeTargets = GlowOutlineTarget.ActiveTargets;
if (activeTargets.Count == 0)
return;
var resourceData = frameData.Get();
var cameraData = frameData.Get();
var descriptor = cameraData.cameraTargetDescriptor;
descriptor.depthBufferBits = 0;
descriptor.msaaSamples = 1;
// Create HDR descriptor for glow textures (allows emission values > 1)
var hdrDesc = descriptor;
hdrDesc.graphicsFormat = GraphicsFormat.R16G16B16A16_SFloat; // HDR format
// Create silhouette texture (full res, HDR for emission support)
var silhouetteTexture = UniversalRenderer.CreateRenderGraphTexture(renderGraph, hdrDesc,
"_FullOutlineSilhouette", false);
// Create object depth texture (full res) - stores linear depth of outlined objects
var objectDepthTexture = UniversalRenderer.CreateRenderGraphTexture(renderGraph, descriptor,
"_FullOutlineObjectDepth", false);
// Create dilated depth textures (full res) for ping-pong dilation
var dilatedDepth1 = UniversalRenderer.CreateRenderGraphTexture(renderGraph, descriptor,
"_FullOutlineDilatedDepth1", false);
var dilatedDepth2 = UniversalRenderer.CreateRenderGraphTexture(renderGraph, descriptor,
"_FullOutlineDilatedDepth2", false);
// Create occlusion mask texture (full res)
var occlusionTexture = UniversalRenderer.CreateRenderGraphTexture(renderGraph, descriptor,
"_FullOutlineOcclusion", false);
// Create blur textures (downsampled, HDR for emission support)
var blurDesc = hdrDesc;
blurDesc.width = Mathf.Max(1, blurDesc.width / settings.downsample);
blurDesc.height = Mathf.Max(1, blurDesc.height / settings.downsample);
var blurTexture1 = UniversalRenderer.CreateRenderGraphTexture(renderGraph, blurDesc,
"_FullOutlineBlur1", false);
var blurTexture2 = UniversalRenderer.CreateRenderGraphTexture(renderGraph, blurDesc,
"_FullOutlineBlur2", false);
// Create erosion textures (full res, HDR) for silhouette erosion - ping pong
TextureHandle erodedSilhouette1 = TextureHandle.nullHandle;
TextureHandle erodedSilhouette2 = TextureHandle.nullHandle;
if (settings.innerEdgeErosion > 0 && erodeMaterial != null)
{
erodedSilhouette1 = UniversalRenderer.CreateRenderGraphTexture(renderGraph, hdrDesc,
"_FullOutlineErodedSilhouette1", false);
erodedSilhouette2 = UniversalRenderer.CreateRenderGraphTexture(renderGraph, hdrDesc,
"_FullOutlineErodedSilhouette2", false);
}
// ============ Pass 1: Render silhouettes (glow color) ============
using (var builder = renderGraph.AddUnsafePass("FullOutline_Silhouette", out var passData, profilingSampler))
{
passData.silhouetteMaterial = silhouetteMaterial;
passData.silhouetteTexture = silhouetteTexture;
builder.UseTexture(silhouetteTexture, AccessFlags.Write);
builder.SetRenderFunc((SilhouettePassData data, UnsafeGraphContext context) =>
{
var cmd = CommandBufferHelpers.GetNativeCommandBuffer(context.cmd);
cmd.SetRenderTarget(data.silhouetteTexture);
cmd.ClearRenderTarget(true, true, Color.clear);
foreach (var target in GlowOutlineTarget.ActiveTargets)
{
if (target == null || target.Renderers == null)
continue;
data.silhouetteMaterial.SetColor(ColorId, target.GlowColor);
data.silhouetteMaterial.SetFloat(IntensityId, target.GlowIntensity);
foreach (var renderer in target.Renderers)
{
if (renderer == null)
continue;
for (int submesh = 0; submesh < renderer.sharedMaterials.Length; submesh++)
{
cmd.DrawRenderer(renderer, data.silhouetteMaterial, submesh, 0); // Pass 0 = Silhouette
}
}
}
});
}
// ============ Pass 1b: Erode silhouette (shrink the mask to hide aliased inner edge) ============
// The eroded silhouette will be used for subtraction, while the original is used for blur
TextureHandle finalErodedSilhouette = silhouetteTexture; // Default to original if no erosion
if (settings.innerEdgeErosion > 0 && erodeMaterial != null && erodedSilhouette1.IsValid())
{
TextureHandle erodeSource = silhouetteTexture;
TextureHandle erodeDest = erodedSilhouette1;
for (int i = 0; i < settings.innerEdgeErosion; i++)
{
int iterIndex = i;
TextureHandle src = erodeSource;
TextureHandle dst = erodeDest;
using (var builder = renderGraph.AddUnsafePass($"FullOutline_Erode_{i}", out var passData, profilingSampler))
{
passData.material = erodeMaterial;
passData.source = src;
passData.destination = dst;
passData.offset = 1.0f; // 1 pixel per iteration
passData.passIndex = 0;
builder.UseTexture(src, AccessFlags.Read);
builder.UseTexture(dst, AccessFlags.Write);
builder.SetRenderFunc((BlitPassData data, UnsafeGraphContext context) =>
{
var cmd = CommandBufferHelpers.GetNativeCommandBuffer(context.cmd);
data.material.SetFloat(OffsetId, data.offset);
Blitter.BlitCameraTexture(cmd, data.source, data.destination, data.material, data.passIndex);
});
}
// Ping-pong between textures
if (i == 0)
{
erodeSource = erodedSilhouette1;
erodeDest = erodedSilhouette2;
}
else
{
var temp = erodeSource;
erodeSource = erodeDest;
erodeDest = temp;
}
}
finalErodedSilhouette = erodeSource;
}
// ============ Pass 2: Render object depth ============
using (var builder = renderGraph.AddUnsafePass("FullOutline_ObjectDepth", out var passData, profilingSampler))
{
passData.silhouetteMaterial = silhouetteMaterial;
passData.depthTexture = objectDepthTexture;
builder.UseTexture(objectDepthTexture, AccessFlags.Write);
builder.SetRenderFunc((ObjectDepthPassData data, UnsafeGraphContext context) =>
{
var cmd = CommandBufferHelpers.GetNativeCommandBuffer(context.cmd);
cmd.SetRenderTarget(data.depthTexture);
// Clear to 1.0 (far plane) so unwritten pixels don't trigger false occlusion
cmd.ClearRenderTarget(true, true, Color.white);
foreach (var target in GlowOutlineTarget.ActiveTargets)
{
if (target == null || target.Renderers == null)
continue;
foreach (var renderer in target.Renderers)
{
if (renderer == null)
continue;
for (int submesh = 0; submesh < renderer.sharedMaterials.Length; submesh++)
{
cmd.DrawRenderer(renderer, data.silhouetteMaterial, submesh, 1); // Pass 1 = ObjectDepth
}
}
}
});
}
// ============ Pass 3: Dilate object depth (expand outward using min-filter) ============
TextureHandle dilateSource = objectDepthTexture;
TextureHandle dilateDest = dilatedDepth1;
for (int i = 0; i < settings.depthDilationIterations; i++)
{
int iterIndex = i;
TextureHandle src = dilateSource;
TextureHandle dst = dilateDest;
using (var builder = renderGraph.AddUnsafePass($"FullOutline_Dilate_{i}", out var passData, profilingSampler))
{
passData.material = depthDilateMaterial;
passData.source = src;
passData.destination = dst;
passData.offset = settings.dilationSpread;
passData.passIndex = 0;
builder.UseTexture(src, AccessFlags.Read);
builder.UseTexture(dst, AccessFlags.Write);
builder.SetRenderFunc((BlitPassData data, UnsafeGraphContext context) =>
{
var cmd = CommandBufferHelpers.GetNativeCommandBuffer(context.cmd);
data.material.SetFloat(OffsetId, data.offset);
Blitter.BlitCameraTexture(cmd, data.source, data.destination, data.material, data.passIndex);
});
}
// Ping-pong between textures
if (i == 0)
{
dilateSource = dilatedDepth1;
dilateDest = dilatedDepth2;
}
else
{
var temp = dilateSource;
dilateSource = dilateDest;
dilateDest = temp;
}
}
TextureHandle finalDilatedDepth = dilateSource;
// ============ Pass 4: Generate occlusion mask (compare scene depth vs dilated object depth) ============
using (var builder = renderGraph.AddUnsafePass("FullOutline_OcclusionCompare", out var passData, profilingSampler))
{
passData.material = compositeMaterial;
passData.source = finalDilatedDepth;
passData.destination = occlusionTexture;
passData.biasMin = settings.occlusionBiasMin;
passData.biasScale = settings.occlusionBiasScale;
builder.UseTexture(finalDilatedDepth, AccessFlags.Read);
builder.UseTexture(occlusionTexture, AccessFlags.Write);
if (resourceData.cameraDepthTexture.IsValid())
builder.UseTexture(resourceData.cameraDepthTexture, AccessFlags.Read);
builder.SetRenderFunc((OcclusionComparePassData data, UnsafeGraphContext context) =>
{
var cmd = CommandBufferHelpers.GetNativeCommandBuffer(context.cmd);
data.material.SetFloat(OcclusionBiasMinId, data.biasMin);
data.material.SetFloat(OcclusionBiasScaleId, data.biasScale);
Blitter.BlitCameraTexture(cmd, data.source, data.destination, data.material, 3); // Pass 3 = DepthOcclusion
});
}
// ============ Pass 5: Downsample silhouette to blur texture ============
using (var builder = renderGraph.AddUnsafePass("FullOutline_Downsample", out var passData, profilingSampler))
{
passData.source = silhouetteTexture;
passData.destination = blurTexture1;
builder.UseTexture(silhouetteTexture, AccessFlags.Read);
builder.UseTexture(blurTexture1, AccessFlags.Write);
builder.SetRenderFunc((BlitPassData data, UnsafeGraphContext context) =>
{
var cmd = CommandBufferHelpers.GetNativeCommandBuffer(context.cmd);
Blitter.BlitCameraTexture(cmd, data.source, data.destination);
});
}
// ============ Pass 6: Blur iterations for glow ============
TextureHandle currentSource = blurTexture1;
TextureHandle currentDest = blurTexture2;
for (int i = 0; i < settings.blurIterations; i++)
{
int iterIndex = i;
TextureHandle src = currentSource;
TextureHandle dst = currentDest;
using (var builder = renderGraph.AddUnsafePass($"FullOutline_Blur_{i}", out var passData, profilingSampler))
{
passData.material = blurMaterial;
passData.source = src;
passData.destination = dst;
passData.offset = settings.blurSpread + iterIndex;
passData.passIndex = 0;
builder.UseTexture(src, AccessFlags.Read);
builder.UseTexture(dst, AccessFlags.Write);
builder.SetRenderFunc((BlitPassData data, UnsafeGraphContext context) =>
{
var cmd = CommandBufferHelpers.GetNativeCommandBuffer(context.cmd);
data.material.SetFloat(OffsetId, data.offset);
Blitter.BlitCameraTexture(cmd, data.source, data.destination, data.material, data.passIndex);
});
}
var temp = currentSource;
currentSource = currentDest;
currentDest = temp;
}
TextureHandle finalBlur = currentSource;
TextureHandle tempTarget = currentDest;
// ============ Pass 7: Debug visualization OR composite ============
using (var builder = renderGraph.AddUnsafePass("FullOutline_Composite", out var passData, profilingSampler))
{
passData.compositeMaterial = compositeMaterial;
passData.glowTexture = finalBlur;
passData.occlusionTexture = occlusionTexture;
passData.silhouetteTexture = silhouetteTexture;
passData.erodedSilhouetteTexture = finalErodedSilhouette; // Use eroded silhouette for subtraction
passData.objectDepthTexture = objectDepthTexture;
passData.dilatedDepthTexture = finalDilatedDepth;
passData.tempTexture = tempTarget;
passData.sceneColor = resourceData.activeColorTexture;
passData.glowIntensity = settings.glowIntensity;
passData.debugMode = settings.debugMode;
builder.UseTexture(finalBlur, AccessFlags.Read);
builder.UseTexture(silhouetteTexture, AccessFlags.Read);
if (finalErodedSilhouette.IsValid() && !finalErodedSilhouette.Equals(silhouetteTexture))
builder.UseTexture(finalErodedSilhouette, AccessFlags.Read);
builder.UseTexture(occlusionTexture, AccessFlags.Read);
builder.UseTexture(objectDepthTexture, AccessFlags.Read);
builder.UseTexture(finalDilatedDepth, AccessFlags.Read);
builder.UseTexture(tempTarget, AccessFlags.ReadWrite);
builder.UseTexture(resourceData.activeColorTexture, AccessFlags.ReadWrite);
builder.SetRenderFunc((CompositePassData data, UnsafeGraphContext context) =>
{
var cmd = CommandBufferHelpers.GetNativeCommandBuffer(context.cmd);
// Debug modes: show intermediate textures
if (data.debugMode != GlowOutlineRendererFeature.DebugMode.None)
{
TextureHandle debugTex = data.debugMode switch
{
GlowOutlineRendererFeature.DebugMode.Silhouette => data.silhouetteTexture,
GlowOutlineRendererFeature.DebugMode.ErodedSilhouette => data.erodedSilhouetteTexture,
GlowOutlineRendererFeature.DebugMode.ObjectDepth => data.objectDepthTexture,
GlowOutlineRendererFeature.DebugMode.DilatedDepth => data.dilatedDepthTexture,
GlowOutlineRendererFeature.DebugMode.OcclusionMask => data.occlusionTexture,
GlowOutlineRendererFeature.DebugMode.BlurredGlow => data.glowTexture,
_ => data.glowTexture
};
Blitter.BlitCameraTexture(cmd, debugTex, data.sceneColor);
return;
}
// Build final glow step by step:
// 1. Copy blurred glow to temp texture
Blitter.BlitCameraTexture(cmd, data.glowTexture, data.tempTexture, data.compositeMaterial, 2); // Pass 2 = Copy
// 2. Subtract eroded silhouette mask (removes glow from object center, leaving outer glow)
// Using eroded silhouette allows glow to extend slightly onto the object, hiding aliased inner edge
Blitter.BlitCameraTexture(cmd, data.erodedSilhouetteTexture, data.tempTexture, data.compositeMaterial, 1); // Pass 1 = SubtractMask
// 3. Subtract occlusion mask (removes glow where other objects are in front)
Blitter.BlitCameraTexture(cmd, data.occlusionTexture, data.tempTexture, data.compositeMaterial, 1); // Pass 1 = SubtractMask
// 4. Additively blend the final glow onto the scene
data.compositeMaterial.SetFloat(GlowIntensityId, data.glowIntensity);
Blitter.BlitCameraTexture(cmd, data.tempTexture, data.sceneColor, data.compositeMaterial, 0); // Pass 0 = AdditiveBlend
});
}
}
}
}