Editor utility that attaches a CommandList to a CameraComponent to preview a compiled post-process material by grabbing the frame (and optionally depth) into render attributes and blitting the material over the finished frame. It manages attachment, rebuilding the command list when settings change, and detaching/disposing.
using Editor.Prism.Core;
using Sandbox.Rendering;
using EngineMaterial = Sandbox.Material;
namespace Editor.Prism.Preview;
/// <summary>
/// Previews a <see cref="ShaderDomain.PostProcess"/> graph by blitting its compiled material over the
/// finished frame.
/// <para>
/// A surface shader and a post-process shader are drawn by completely different machinery and neither
/// can stand in for the other: a surface shader expects the model vertex layout and world-space
/// transforms, while a post-process shader expects a full-screen quad in normalised device
/// coordinates. So instead of putting the material on the preview mesh, this attaches a
/// <see cref="CommandList"/> to the preview camera after post-processing, grabs the frame (and, by
/// default, the depth buffer) into the attributes the engine's screen-space shaders read, and blits.
/// </para>
/// <para>
/// The subject mesh stays in the scene behind the effect, wearing a plain reflective material, so
/// there is real geometry, real lighting and real depth for the effect to act on rather than an empty
/// background.
/// </para>
/// </summary>
public sealed class PostProcessPreview : IDisposable
{
/// <summary>Attribute the grabbed frame colour is bound to. Matches the engine's own effects.</summary>
public const string ColorBufferName = "ColorBuffer";
/// <summary>Attribute the grabbed depth buffer is bound to.</summary>
public const string DepthBufferName = "DepthBuffer";
CommandList _list;
CameraComponent _camera;
Stage _attachedStage;
bool _attached;
bool _enabled;
EngineMaterial _material;
RenderAttributes _attributes;
/// <summary>Which render stage the blit is inserted at.</summary>
public Stage RenderStage { get; set; } = Stage.AfterPostProcess;
/// <summary>Ordering within the stage. High so the effect lands on top of the editor's own passes.</summary>
public int Order { get; set; } = 1000;
/// <summary>Whether the frame colour is grabbed into <see cref="ColorBufferName"/> before the blit.</summary>
public bool GrabColor { get; set; } = true;
/// <summary>Whether the depth buffer is grabbed into <see cref="DepthBufferName"/> before the blit.</summary>
public bool GrabDepth { get; set; } = true;
/// <summary>
/// Extra per-blit attributes, normally the preview subject's own attribute block, so the graph's
/// live uniforms reach the effect. Assigning rebuilds the command list, because the block is
/// captured into it at record time rather than read on each frame.
/// </summary>
public RenderAttributes Attributes
{
get => _attributes;
set
{
if ( ReferenceEquals( _attributes, value ) ) return;
_attributes = value;
Refresh();
}
}
/// <summary>True when a command list is currently attached to a camera.</summary>
public bool IsAttached => _attached;
/// <summary>Whether the effect is drawn at all.</summary>
public bool Enabled
{
get => _enabled;
set
{
if ( _enabled == value ) return;
_enabled = value;
Refresh();
}
}
/// <summary>The compiled post-process material. Null detaches the command list entirely.</summary>
public EngineMaterial Material
{
get => _material;
set
{
if ( _material == value ) return;
_material = value;
Refresh();
}
}
/// <summary>
/// Point the effect at a camera. Passing a different camera detaches from the previous one first,
/// so a viewport that rebuilds its scene never leaves an orphaned command list behind.
/// </summary>
public void Attach( CameraComponent camera )
{
if ( ReferenceEquals( _camera, camera ) )
{
Refresh();
return;
}
Detach();
_camera = camera;
Refresh();
}
/// <summary>
/// Rebuild the command list from the current material and settings. Cheap; safe to call on every
/// compile and every settings change.
/// </summary>
public void Refresh()
{
if ( !_enabled || _material is null || !_camera.IsValid() )
{
Detach();
return;
}
PrismLog.Guard( "Rebuilding the post-process preview", () =>
{
_list ??= new CommandList( "Prism PostProcess Preview" );
if ( !_attached || _attachedStage != RenderStage )
{
if ( _attached ) _camera.RemoveCommandList( _list );
_camera.AddCommandList( _list, RenderStage, Order );
_attached = true;
_attachedStage = RenderStage;
}
_list.Reset();
if ( GrabColor ) _list.Attributes.GrabFrameTexture( ColorBufferName );
if ( GrabDepth ) _list.Attributes.GrabDepthTexture( DepthBufferName );
_list.Blit( _material, _attributes );
} );
}
/// <summary>Remove the command list from its camera. Idempotent.</summary>
public void Detach()
{
if ( !_attached || _list is null )
{
_attached = false;
return;
}
PrismLog.Guard( "Detaching the post-process preview", () =>
{
if ( _camera.IsValid() ) _camera.RemoveCommandList( _list );
} );
_attached = false;
}
/// <summary>Detach and drop the command list. Must run before the preview scene is destroyed.</summary>
public void Dispose()
{
Detach();
_list = null;
_camera = null;
_material = null;
_attributes = null;
}
}