UI/HudCanvas.cs

A UI canvas that renders HUD sections into an offscreen render target, applies a two-pass blur glow and composites scanline/glow effects onto the screen. Manages per-frame render targets, command lists and draws only regions that need painting.

Native Interop
using Sandbox.Rendering;
using Sandbox.UI;

namespace Sandbox;

/// <summary>
/// HUD layer with glow & scanline effects
/// </summary>
public sealed class HudCanvas : Panel, IPanelDraw
{
	/// <summary>
	/// The component that owns the effect settings
	/// </summary>
	public HudLayer Layer { get; set; }

	readonly List<HudSection> _sections = new();

	Texture _target;
	RenderTarget _renderTarget;
	CommandList _paintCommands;
	CameraComponent _camera;
	Material _material;

	// half resolution ping-pong targets for the glow blur
	Texture _glowA, _glowB;
	RenderTarget _glowATarget, _glowBTarget;
	Material _glowMaterial;

	/// <summary>
	/// Screen rects the sections draw into this frame. Empty means there is nothing to paint
	/// </summary>
	readonly List<Rect> _regions = new();

	bool _paintRecorded;

	/// <summary>
	/// Canvas size in whole pixels, which is what the paint target is created with
	/// </summary>
	public Vector2 PixelSize => new( (int)Box.Rect.Width, (int)Box.Rect.Height );

	public HudCanvas()
	{
		Style.Position = PositionMode.Absolute;
		Style.Width = Length.Percent( 100 );
		Style.Height = Length.Percent( 100 );
		Style.PointerEvents = PointerEvents.None;
	}

	/// <summary>
	/// The section of type <typeparamref name="T"/>, added on first use unless <paramref name="create"/> is false (then null if there is none)
	/// </summary>
	public T Section<T>( bool create = true ) where T : HudSection, new()
	{
		foreach ( var section in _sections )
		{
			if ( section is T existing ) return existing;
		}

		if ( !create ) return null;

		var added = new T { Canvas = this };
		_sections.Add( added );
		_sections.Sort( ( a, b ) => a.Order.CompareTo( b.Order ) );
		return added;
	}

	public override void Tick()
	{
		base.Tick();

		if ( !Layer.IsValid() ) return;

		foreach ( var section in _sections )
			section.Tick();

		_regions.Clear();
		foreach ( var section in _sections )
			section.GetCompositeRegions( _regions );

		if ( _regions.Count == 0 )
		{
			if ( _paintRecorded ) _paintCommands.Reset();
			_paintRecorded = false;
			return;
		}

		PaintTarget();
	}

	void PaintTarget()
	{
		var width = (int)PixelSize.x;
		var height = (int)PixelSize.y;
		if ( width <= 0 || height <= 0 ) return;

		if ( _target is null || _target.Width != width || _target.Height != height )
		{
			_target?.Dispose();
			_target = Texture.CreateRenderTarget().WithSize( width, height ).WithFormat( ImageFormat.RGBA16161616F ).Create();
			_renderTarget = RenderTarget.From( _target );
		}

		_paintCommands ??= new CommandList( GetType().Name );

		var camera = Game.ActiveScene?.Camera;
		if ( camera != _camera )
		{
			_camera?.RemoveCommandList( _paintCommands );
			camera?.AddCommandList( _paintCommands, Stage.AfterViewmodel );
			_camera = camera;
		}

		_paintCommands.Reset();
		_paintRecorded = true;
		_paintCommands.Attributes.Set( "UIGammaOutput", true );
		_paintCommands.Attributes.Set( "UIFrameGrabEncoded", true );
		_paintCommands.SetRenderTarget( _renderTarget );

		using ( var painter = Painter.Begin( _paintCommands, new Rect( 0, 0, width, height ) ) )
		{
			painter.Clear( Color.Transparent );

			foreach ( var section in _sections )
			{
				using var _ = painter.Scope();
				section.PaintTint = Layer.Tint;
				section.Paint( painter );
			}
		}

		BlurGlow( width, height );

		_paintCommands.ClearRenderTarget();
	}

	void BlurGlow( int width, int height )
	{
		var glowWidth = Math.Max( 1, width / 2 );
		var glowHeight = Math.Max( 1, height / 2 );

		if ( _glowA is null || _glowA.Width != glowWidth || _glowA.Height != glowHeight )
		{
			_glowA?.Dispose();
			_glowB?.Dispose();
			_glowA = Texture.CreateRenderTarget().WithSize( glowWidth, glowHeight ).WithFormat( ImageFormat.RGBA16161616F ).Create();
			_glowB = Texture.CreateRenderTarget().WithSize( glowWidth, glowHeight ).WithFormat( ImageFormat.RGBA16161616F ).Create();
			_glowATarget = RenderTarget.From( _glowA );
			_glowBTarget = RenderTarget.From( _glowB );
		}

		_glowMaterial ??= Material.FromShader( "shaders/hud_glow.shader" );

		var reach = Layer.GlowRadius * ScaleToScreen * 0.5f;
		_paintCommands.Attributes.Set( "GlowReach", reach );

		_paintCommands.SetRenderTarget( _glowATarget );
		_paintCommands.Attributes.Set( "GlowSource", _target );
		_paintCommands.Attributes.Set( "GlowExtract", 1 );
		_paintCommands.Attributes.Set( "GlowStep", new Vector2( 1f / glowWidth, 0 ) );
		_paintCommands.Blit( _glowMaterial );

		_paintCommands.SetRenderTarget( _glowBTarget );
		_paintCommands.Attributes.Set( "GlowSource", _glowA );
		_paintCommands.Attributes.Set( "GlowExtract", 0 );
		_paintCommands.Attributes.Set( "GlowStep", new Vector2( 0, 1f / glowHeight ) );
		_paintCommands.Blit( _glowMaterial );
	}

	void IPanelDraw.Draw( CommandList commands )
	{
		if ( !Layer.IsValid() || _regions.Count == 0 || _target is null || _glowB is null ) return;

		_material ??= Material.FromShader( "shaders/hud_composite.shader" );

		var scale = ScaleToScreen;
		var attributes = commands.Attributes;
		attributes.Set( "HudTexture", _target );
		attributes.Set( "HudInvSize", new Vector2( 1f / _target.Width, 1f / _target.Height ) );
		attributes.Set( "GlowTexture", _glowB );
		attributes.Set( "GlowStrength", Layer.GlowStrength );
		attributes.Set( "ScanlineIntensity", Layer.ScanlineIntensity );
		attributes.Set( "ScanlinePeriod", MathF.Max( 2, MathF.Round( Layer.ScanlinePeriod * scale ) ) );
		attributes.Set( "ScanlineThickness", Layer.ScanlineThickness );
		attributes.Set( "ScanlineSoftness", Layer.ScanlineSoftness );
		attributes.SetCombo( "D_BLENDMODE", BlendMode.Normal );

		// The glow is added onto what's behind the HUD
		attributes.GrabFrameTexture( "FrameBufferCopyTexture", Graphics.DownsampleMethod.None );

		foreach ( var region in _regions )
			commands.DrawQuad( region, _material, Color.White );
	}

	public override void OnDeleted()
	{
		if ( _paintCommands is not null ) _camera?.RemoveCommandList( _paintCommands );
		_camera = null;
		_target?.Dispose();
		_glowA?.Dispose();
		_glowB?.Dispose();
		_target = _glowA = _glowB = null;

		base.OnDeleted();
	}
}