GlowOutline.cs
using System;
using System.Collections.Generic;
using Sandbox;
using Sandbox.Rendering;

[Icon( "Accessibility_New" )]
public sealed class GlowOutline : BasePostProcess<GlowOutline>
{
	public enum DownSampleMethods : byte
	{
		Box = 0,
		GaussianBlur = 1,
	}

	public enum OutlinePresets : byte
	{
		Valve,
		Regular,
	}

	[Property, Feature( "Glow Settings" ), Title( "Default Color" )]
	private Color defaultGlowColor = new( 0.10f, 0.32f, 0.79f, 1.00f );

	[Property, Range( 0, 3 ), Step( 1 ), Description( "Changes the resolution of the blur (higher value means lower quality)" ), Feature( "Glow Settings" )]
	private int glowMips = 2;

	[Property, Range( 1.0f, 10.0f ), Step( 0.25f ), Description( "How big you want the glow to be." ), Feature( "Glow Settings" )]
	private float glowSize = 5.0f;

	[Property, Range( 0.25f, 10.0f ), Step( 0.25f ), Description( "How bright you want the glow to be." ), Feature( "Glow Settings" )]
	private float glowIntensity = 5.0f;

	[Property, Feature( "Glow Settings" ), Description( "If enabled, automatically finds all objects with a `Glowable` component and applies a glow effect." )]
	private readonly bool autoFindGlowables = false;

	[Header( "Glow Rendering" )]

	[Property, Title( "Glow Types" ), Description( "Changes the downsample method that is used to create different outline effects." ), Feature( "Glow Settings" )]
	private DownSampleMethods DownSampleMethod { get; set; } = DownSampleMethods.GaussianBlur;

	[Property, Title( "Glow Presets" ), Description( "Some presets if you are lazy." ), Feature( "Glow Settings" )]
	private OutlinePresets OutlinePreset { get; set; } = OutlinePresets.Valve;

	[Property, Title( "Glow Presets" ), Button( "Apply Preset" ), Feature( "Glow Settings" )]
	public void ApplyPreset()
	{
		if ( OutlinePreset == OutlinePresets.Valve )
		{
			glowMips = 1;
			DownSampleMethod = DownSampleMethods.GaussianBlur;
			glowIntensity = 1.75f;
			glowSize = 3.0f;
		}
		else
		{
			glowMips = 0;
			DownSampleMethod = DownSampleMethods.Box;
			glowSize = 2.0f;
			glowIntensity = 1.75f;
		}
	}

	[Property, Title( "Objects" ), Feature( "Objects to Glow" ), InlineEditor( Label = false )]
	private List<GlowObject> objectsToGlow = null;

	private const string MASK_RENDER_TARGET = "MaskRT";

	private const string TMP_RENDER_TARGET = "TmpRT";

	private const string GLOW_COLOR_ATTRIBUTE = "GlowColor";

	private readonly Material maskMaterial = Material.FromShader( "shaders/Mask.shader" );
	private readonly Material compositeMaterial = Material.FromShader( "shaders/Composite.shader" );
	private readonly Material stencilMaterial = Material.FromShader( "shaders/Stencil.shader" );
	private readonly CommandList commandList = new( "GlowOutline" );

	RendererSetup maskRenderSetup = default;
	RendererSetup stencilRenderSetup = default;
	public int Count => objectsToGlow.Count;

	protected override void OnAwake()
	{
		// This SHOULD only happen in the editor since people like to change editor width and height etc.
		if ( Screen.Size.x % 2 == 1 || Screen.Size.y % 2 == 1 )
		{
			Log.Error( $"Glow Outline: To avoid uneven outlines, please use an even screen resolution. This message should only appear in the editor. Current resolution: {Screen.Size}." );
		}

		maskRenderSetup = new RendererSetup
		{
			Material = maskMaterial,
			Transform = null,
			Color = null
		};

		stencilRenderSetup = new RendererSetup
		{
			Material = stencilMaterial,
			Transform = null,
			Color = null
		};

		objectsToGlow ??= new();

		SetTransparentColorToDefault();
	}

	protected override void OnStart()
	{
		if ( !autoFindGlowables ) return;

		FindGlowableObjects();
	}

	public override void Render()
	{
		if ( Count <= 0 ) return;

		commandList.Reset();
		RenderOutlineEffect();
		InsertCommandList( commandList, Stage.AfterTransparent, 1000, "GlowOutline" );
	}

	private void RenderOutlineEffect()
	{
		RenderTargetHandle blurRT = GetBlurRenderTarget();
		try
		{
			for ( int i = 0; i < objectsToGlow.Count; i++ )
			{
				GlowObject glowObject = objectsToGlow[i];

				if ( glowObject.GameObject == null ) continue;

				glowObject.Renderer ??= glowObject.GameObject.GetComponent<ModelRenderer>();
				commandList.DrawRenderer( glowObject.Renderer, stencilRenderSetup );
			}

			Composite( blurRT );
		}
		finally
		{
			commandList.ReleaseRenderTarget( blurRT );
		}
	}

	private void Composite( RenderTargetHandle downScaledRT )
	{
		RenderTargetHandle frameRT = commandList.Attributes.GrabFrameTexture( "SceneTexture" );

		commandList.Attributes.Set( "DownScaledTexture", downScaledRT.ColorTexture );
		commandList.Attributes.Set( "GlowIntensity", glowIntensity );
		commandList.Attributes.Set( "GlowMips", glowMips );
		commandList.Blit( compositeMaterial );

		commandList.ClearRenderTarget();
		commandList.ReleaseRenderTarget( frameRT );
	}

	private RenderTargetHandle GetBlurRenderTarget()
	{
		Graphics.DownsampleMethod downSampleMethod = GetDownSampleMethod();

		RenderTargetHandle blurredRT = CreateMaskRenderTarget( MASK_RENDER_TARGET, 4 );

		try
		{
			RenderTargetHandle tmpRT = commandList.GetRenderTarget( TMP_RENDER_TARGET, ImageFormat.RGBA8888, 4, 1 );
			try
			{
				commandList.GenerateMipMaps( blurredRT, downSampleMethod );

				commandList.SetRenderTarget( tmpRT );
				commandList.Attributes.Set( "TextureToBlur", blurredRT.ColorTexture );
				commandList.Attributes.Set( "GlowSize", glowSize );
				commandList.Blit( Material.FromShader( "shaders/BlurVertical.shader" ) );
				commandList.Attributes.Set( "MipsLevel", glowMips );
				commandList.ClearRenderTarget();

				commandList.GenerateMipMaps( tmpRT, downSampleMethod );

				commandList.SetRenderTarget( blurredRT );
				commandList.Attributes.Set( "VerticalBlurTexture", tmpRT.ColorTexture );
				commandList.Attributes.Set( "MipsLevel", glowMips );
				commandList.Attributes.Set( "GlowSize", glowSize );
				commandList.Blit( Material.FromShader( "shaders/BlurHorizontal.shader" ) );
				commandList.ClearRenderTarget();
			}
			finally
			{
				commandList.ReleaseRenderTarget( tmpRT );
			}
		}
		catch
		{
			commandList.ReleaseRenderTarget( blurredRT );
			throw;
		}

		commandList.ClearRenderTarget();

		return blurredRT;
	}

	private RenderTargetHandle CreateMaskRenderTarget( string name, int mipsLevel = 1 )
	{
		RenderTargetHandle maskRT = commandList.GetRenderTarget( name, ImageFormat.RGBA8888, mipsLevel, 1 );

		try
		{
			commandList.SetRenderTarget( maskRT );
			commandList.Clear( Color.Transparent);

			for ( int i = 0; i < objectsToGlow.Count; i++ )
			{
				GlowObject glowObject = objectsToGlow[i];

				if ( glowObject.GameObject == null ) continue;

				glowObject.Renderer ??= glowObject.GameObject.GetComponent<ModelRenderer>();

				commandList.Attributes.Set( GLOW_COLOR_ATTRIBUTE, glowObject.Color );
				commandList.DrawRenderer( glowObject.Renderer, maskRenderSetup );
			}

			commandList.ClearRenderTarget();
		}
		catch
		{
			commandList.ReleaseRenderTarget( maskRT );
			throw;
		}

		return maskRT;
	}

	private void SetTransparentColorToDefault()
	{
		for ( int i = 0; i < objectsToGlow.Count; i++ )
		{
			if ( objectsToGlow[i].Color != Color.Transparent ) continue;

			objectsToGlow[i].Color = defaultGlowColor;
		}
	}

	/// <summary>
	/// Changes the glow color of a specific GameObject.
	/// </summary>

	public void SetGlowColor( GameObject item, Color color )
	{
		for ( int i = 0; i < objectsToGlow.Count; i++ )
		{
			if ( objectsToGlow[i].GameObject != item ) continue;

			objectsToGlow[i].Color = color;
			break;
		}
	}

	/// <summary>
	/// Returns the GlowObject for a specific GameObject if it exists.
	/// </summary>

	public GlowObject GetGlowObject( GameObject item )
	{
		for ( int i = 0; i < objectsToGlow.Count; i++ )
		{
			if ( objectsToGlow[i].GameObject == item ) return objectsToGlow[i];
		}

		return new GlowObject( null, null, null );
	}

	public List<GlowObject> GlowingObjects()
	{
		return objectsToGlow;
	}

	/// <summary>
	/// Adds a GameObject with the default glow color.
	/// </summary>
	public void Add( GameObject item )
	{
		Add( item, defaultGlowColor );
	}

	/// <summary>
	/// Tries to add a GameObject with a specific glow color. 
	/// Returns false if the GameObject is already present.
	/// </summary>
	public bool TryAdd( GameObject item, Color color )
	{
		bool itemExists = Contains( item );

		if ( itemExists ) return false;

		Add( item, color );

		return true;
	}

	/// <summary>
	/// Tries to add a GameObject with the default glow color. 
	/// Returns false if the GameObject is already present.
	/// </summary>
	public bool TryAdd( GameObject item )
	{
		return TryAdd( item, defaultGlowColor );
	}

	/// <summary>
	/// Adds a GameObject with the specified glow color and associated ModelRenderer.
	/// </summary>
	public void Add( GameObject item, Color color )
	{
		objectsToGlow.Add( new GlowObject( item, color, item.GetComponent<ModelRenderer>() ) );
	}

	/// <summary>
	/// Checks if the specified GameObject is already in the list.
	/// </summary>
	public bool Contains( GameObject item )
	{
		for ( int i = 0; i < objectsToGlow.Count; i++ )
		{
			if ( objectsToGlow[i].GameObject == item ) return true;
		}

		return false;
	}

	/// <summary>
	/// Removes the specified GameObject from the list, if it exists.
	/// </summary>
	public void Remove( GameObject item )
	{
		for ( int i = 0; i < objectsToGlow.Count; i++ )
		{
			if ( objectsToGlow[i].GameObject != item ) continue;

			RemoveAt( i );
			break;
		}
	}

	/// <summary>
	/// Removes the GameObject at the specified index.
	/// </summary>
	public void RemoveAt( int index )
	{
		objectsToGlow.RemoveAt( index );
	}

	/// <summary>
	/// Clears all GameObjects from the list.
	/// </summary>
	public void Clear()
	{
		objectsToGlow.Clear();
	}

	private Graphics.DownsampleMethod GetDownSampleMethod()
	{
		return (Graphics.DownsampleMethod)DownSampleMethod;
	}

	private void FindGlowableObjects()
	{
		IEnumerable<Glowable> glowableObjects = Scene.GetAll<Glowable>();

		foreach ( Glowable item in glowableObjects )
		{
			if ( !item.AddOnStart || Contains( item.GameObject ) ) continue;

			item.AddSelf( this );
		}
	}

	protected override void OnDisabled()
	{
		commandList.Reset();
	}
}

//WARNING: If you add / remove any fields from this class, it will remove all objects in the list.
public class GlowObject
{
	public GameObject GameObject { get; set; }
	[Hide]
	public Renderer Renderer { get; set; }

	[Description( "If kept transparent (#00000000) it will set it to default color automatically" )]
	public Color Color { get; set; } = Color.White;

	public GlowObject()
	{

	}

	public GlowObject( Color color )
	{
		Color = color;
	}

	public GlowObject( GameObject gameObject, Color color, Renderer renderer )
	{
		Color = color;
		GameObject = gameObject;
		Renderer = renderer;
	}
}