Defines PreviewChannel enum and PreviewChannelInfo record, and a static PreviewChannels utility that lists all preview debug channels, maps ids/titles/icons, parses serialized ids, applies channel to render attributes and camera.
using Editor.Prism.Core;
namespace Editor.Prism.Preview;
/// <summary>
/// What the preview viewport displays instead of the shaded result.
/// <para>
/// The numeric value of each member is the value pushed into
/// <see cref="PrismConstants.ChannelAttribute"/>, so it is part of the generated-shader contract and
/// must never be renumbered. New channels are appended.
/// </para>
/// </summary>
public enum PreviewChannel
{
/// <summary>The shaded result. The default.</summary>
Final = 0,
/// <summary>Base colour, linear.</summary>
Albedo = 1,
/// <summary>Coverage.</summary>
Opacity = 2,
/// <summary>Shading normal, in world space.</summary>
NormalWorld = 3,
/// <summary>Shading normal, as the graph produced it, in tangent space.</summary>
NormalTangent = 4,
/// <summary>Microfacet roughness.</summary>
Roughness = 5,
/// <summary>Dielectric to conductor blend.</summary>
Metalness = 6,
/// <summary>Baked occlusion multiplier.</summary>
AmbientOcclusion = 7,
/// <summary>Additive emissive colour.</summary>
Emission = 8,
/// <summary>Light transmitted through the surface.</summary>
Transmission = 9,
/// <summary>Where the per-instance tint applies.</summary>
TintMask = 10,
/// <summary>The first texture coordinate set, drawn as red/green.</summary>
Uv0 = 11,
/// <summary>The second texture coordinate set, drawn as red/green.</summary>
Uv1 = 12,
/// <summary>Interpolated vertex colour.</summary>
VertexColor = 13,
/// <summary>World position, remapped into a visible range.</summary>
WorldPosition = 14,
/// <summary>Screen-space derivatives of the first texture coordinate set.</summary>
Derivatives = 15,
/// <summary>The mip level the hardware would choose for the first texture coordinate set.</summary>
MipLevel = 16,
/// <summary>How many times each pixel was shaded.</summary>
Overdraw = 17,
/// <summary>The engine's shading-cost heat map.</summary>
ShadingComplexity = 18,
/// <summary>Triangle edges over the shaded result.</summary>
Wireframe = 19
}
/// <summary>
/// Everything the UI and the viewport need to know about one <see cref="PreviewChannel"/>.
/// </summary>
/// <param name="Channel">The channel this describes.</param>
/// <param name="Id">Stable identifier written into the document's <c>preview.channel</c> field.</param>
/// <param name="Title">Short label for the channel strip.</param>
/// <param name="Icon">Material icon name.</param>
/// <param name="Description">Tooltip text.</param>
public readonly record struct PreviewChannelInfo(
PreviewChannel Channel, string Id, string Title, string Icon, string Description )
{
/// <summary>
/// The engine's own debug visualisation for this channel, when there is one. Those channels work
/// without any cooperation from the generated shader, which is why they are preferred.
/// </summary>
public SceneCameraDebugMode? DebugMode { get; init; }
/// <summary>True when the channel is drawn by switching the camera into wireframe.</summary>
public bool Wireframe { get; init; }
/// <summary>
/// True when the channel is only meaningful if the generated preview shader implements the
/// <see cref="PrismConstants.ChannelAttribute"/> switch. Those degrade to the shaded result.
/// </summary>
public bool NeedsShaderSupport => DebugMode is null && !Wireframe && Channel != PreviewChannel.Final;
/// <summary>The integer pushed into the channel attribute.</summary>
public int Value => (int)Channel;
/// <inheritdoc/>
public override string ToString() => Title;
}
/// <summary>
/// The debug-channel table: identity, presentation, the attribute value each channel pushes, and the
/// engine debug mode each one maps onto.
/// <para>
/// Two mechanisms drive a channel and they compose. Channels the engine already visualises
/// (<c>Albedo</c>, world normals, roughness, occlusion, UVs, overdraw, shading complexity) are served
/// by <see cref="SceneCameraDebugMode"/>, which needs nothing from the generated shader and therefore
/// works on any graph. Everything else pushes <see cref="PrismConstants.ChannelAttribute"/> and relies
/// on the preview build of the shader implementing the switch; when it does not, the viewport simply
/// keeps showing the shaded result rather than going black.
/// </para>
/// </summary>
public static class PreviewChannels
{
static readonly PreviewChannelInfo[] s_all =
[
new( PreviewChannel.Final, "final", "Final", "image",
"The shaded result, exactly as the engine renders it." ),
new( PreviewChannel.Albedo, "albedo", "Albedo", "palette",
"Base colour before lighting." )
{ DebugMode = SceneCameraDebugMode.Albedo },
new( PreviewChannel.Opacity, "opacity", "Opacity", "opacity",
"Coverage. Drives alpha test and translucency." ),
new( PreviewChannel.NormalWorld, "normal", "Normal", "shuffle",
"Shading normal in world space." )
{ DebugMode = SceneCameraDebugMode.NormalMap },
new( PreviewChannel.NormalTangent, "normal.tangent", "Normal (T)", "explore",
"The tangent-space normal the graph produced, before it is rotated into world space." ),
new( PreviewChannel.Roughness, "roughness", "Roughness", "texture",
"Microfacet roughness." )
{ DebugMode = SceneCameraDebugMode.Roughness },
new( PreviewChannel.Metalness, "metalness", "Metalness", "hardware",
"Dielectric to conductor blend." ),
new( PreviewChannel.AmbientOcclusion, "ao", "AO", "radio_button_checked",
"Baked ambient occlusion." )
{ DebugMode = SceneCameraDebugMode.AmbientOcclusion },
new( PreviewChannel.Emission, "emission", "Emission", "flare",
"Additive emissive colour." ),
new( PreviewChannel.Transmission, "transmission", "Transmission", "deblur",
"Light transmitted through the surface." )
{ DebugMode = SceneCameraDebugMode.Transmission },
new( PreviewChannel.TintMask, "tintmask", "Tint Mask", "format_color_fill",
"Where the per-instance tint applies." ),
new( PreviewChannel.Uv0, "uv0", "UV0", "gradient",
"The first texture coordinate set." )
{ DebugMode = SceneCameraDebugMode.ShowUV },
new( PreviewChannel.Uv1, "uv1", "UV1", "grid_on",
"The second texture coordinate set." ),
new( PreviewChannel.VertexColor, "vertexcolor", "Vertex Colour", "colorize",
"Interpolated vertex colour." ),
new( PreviewChannel.WorldPosition, "worldpos", "World Pos", "public",
"World position, wrapped into a visible range." ),
new( PreviewChannel.Derivatives, "derivatives", "Derivatives", "show_chart",
"Screen-space derivatives of UV0. Flat areas mean a mip-selection problem." ),
new( PreviewChannel.MipLevel, "miplevel", "Mip Level", "layers",
"The mip level the hardware picks for UV0." ),
new( PreviewChannel.Overdraw, "overdraw", "Overdraw", "filter_none",
"How many times each pixel was shaded." )
{ DebugMode = SceneCameraDebugMode.Overdraw },
new( PreviewChannel.ShadingComplexity, "complexity", "Complexity", "whatshot",
"The engine's shading-cost heat map." )
{ DebugMode = SceneCameraDebugMode.QuadOverdraw },
new( PreviewChannel.Wireframe, "wireframe", "Wireframe", "grid_4x4",
"Triangle edges, to check tessellation and vertex displacement." )
{ Wireframe = true }
];
/// <summary>Every channel, in display order.</summary>
public static IReadOnlyList<PreviewChannelInfo> All => s_all;
/// <summary>The render-attribute name the channel index is pushed under.</summary>
public static string AttributeName => PrismConstants.ChannelAttribute;
/// <summary>Metadata for a channel. Never throws; an unknown value describes itself as Final.</summary>
public static PreviewChannelInfo Info( PreviewChannel channel )
{
foreach ( var info in s_all )
{
if ( info.Channel == channel ) return info;
}
return s_all[0];
}
/// <summary>The stable id a channel is serialized under.</summary>
public static string Id( PreviewChannel channel ) => Info( channel ).Id;
/// <summary>Short label for the channel strip.</summary>
public static string Title( PreviewChannel channel ) => Info( channel ).Title;
/// <summary>Material icon name for the channel strip.</summary>
public static string Icon( PreviewChannel channel ) => Info( channel ).Icon;
/// <summary>
/// Parse a serialized channel id. Null, empty and unrecognised values are
/// <see cref="PreviewChannel.Final"/>, so an older or newer document never breaks the viewport.
/// </summary>
public static PreviewChannel Parse( string id )
{
if ( string.IsNullOrWhiteSpace( id ) ) return PreviewChannel.Final;
var trimmed = id.Trim();
foreach ( var info in s_all )
{
if ( string.Equals( info.Id, trimmed, StringComparison.OrdinalIgnoreCase ) ) return info.Channel;
}
// Tolerate the enum spelling too, so a hand-edited document behaves.
foreach ( var info in s_all )
{
if ( string.Equals( info.Channel.ToString(), trimmed, StringComparison.OrdinalIgnoreCase ) ) return info.Channel;
}
return PreviewChannel.Final;
}
/// <summary>True when the id names a channel this build knows about.</summary>
public static bool IsKnown( string id ) =>
!string.IsNullOrWhiteSpace( id ) && ( Parse( id ) != PreviewChannel.Final ||
string.Equals( id.Trim(), "final", StringComparison.OrdinalIgnoreCase ) );
/// <summary>
/// Push the channel selection into a render-attribute block. Always sets the attribute, even for
/// <see cref="PreviewChannel.Final"/>, so a shader that does implement the switch is reset properly
/// when the user goes back to the shaded view.
/// </summary>
public static void Apply( PreviewChannel channel, RenderAttributes attributes )
{
if ( attributes is null ) return;
attributes.Set( PrismConstants.ChannelAttribute, (int)channel );
}
/// <summary>
/// Apply the parts of a channel the engine can render on its own: the tools visualisation mode and
/// wireframe. Returns true when the engine took responsibility for the channel, which is the
/// viewport's cue that it does not matter whether the generated shader implements the switch.
/// </summary>
public static bool Apply( PreviewChannel channel, CameraComponent camera )
{
if ( !camera.IsValid() ) return false;
var info = Info( channel );
return PrismLog.Guard( "Applying the preview debug channel", () =>
{
camera.DebugMode = info.DebugMode ?? SceneCameraDebugMode.Normal;
camera.WireframeMode = info.Wireframe;
return info.DebugMode is not null || info.Wireframe;
} );
}
}