Editor/PbrMapGeneratorWindow.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Editor;
using Sandbox;
using Button = Editor.Button;
using Checkbox = Editor.Checkbox;
using ComboBox = Editor.ComboBox;
using Label = Editor.Label;
using LineEdit = Editor.LineEdit;
using TextEdit = Editor.TextEdit;
using Widget = Editor.Widget;
[Dock( "Editor", "PBR Generator", "texture" )]
public sealed class PbrMapGeneratorWindow : Widget
{
public static PbrMapGeneratorWindow Current { get; private set; }
private PbrGeneratorSettings settings = new();
private readonly Dictionary<PbrMapType, Button> mapButtons = new();
private PbrPreviewWidget previewWidget;
private PbrMaterialPreviewWidget materialPreviewWidget;
private Splitter mainSplitter;
private Splitter contentSplitter;
private Splitter previewSplitter;
private ScrollArea inspectorScroll;
private TextEdit consoleOutput;
private Label currentFileLabel;
private Label imageInfoLabel;
private Label noteLabel;
private LineEdit outputFolderEdit;
private Bitmap sourceBitmap;
private PbrGeneratorResult generatedResult;
private string sourceImagePath;
private string sourceDisplayName;
private string outputFolder;
private string lastExportPath;
private bool outputFolderFollowsSource = true;
private bool generationQueued;
private bool generationRunning;
private bool queuedGenerationWasManual;
private bool windowDestroyed;
private bool experimentalMaterialPreviewEnabled;
private bool autoRefreshMaterialPreview;
private bool parallaxMaterialPreviewEnabled;
private DateTime queuedGenerationTime;
private int latestGenerationRequestId;
private int runningGenerationRequestId;
private PbrMapType selectedMap = PbrMapType.Albedo;
private PbrPreviewLayout previewLayout = PbrPreviewLayout.Atlas;
private PbrPreviewShape previewShape = PbrPreviewShape.Sphere;
private const int InspectorLabelWidth = 132;
private const string MainSplitterCookie = "SeamLessPbrGenerator.MainSplitter";
private const string ContentSplitterCookie = "SeamLessPbrGenerator.ContentSplitter";
private const string PreviewSplitterCookie = "SeamLessPbrGenerator.PreviewSplitter";
private static readonly PbrPreviewLayout[] PreviewLayoutComboOrder =
{
PbrPreviewLayout.Atlas,
PbrPreviewLayout.Single,
PbrPreviewLayout.Tiled2X2,
PbrPreviewLayout.Tiled3X3
};
public PbrMapGeneratorWindow( Widget parent ) : base( parent )
{
Current = this;
Name = "PBR Generator";
WindowTitle = "PBR Generator";
MinimumSize = new Vector2( 1040, 680 );
AcceptDrops = true;
ApplySavedPreferences( PbrGeneratorPreferences.Load() );
SetWindowIcon( "texture" );
BuildLayout();
LogMessage( "Ready. Open an albedo texture, drag one into the preview, or use the current suite texture." );
ShowStatus( "Ready", 4f );
}
public override void OnDestroyed()
{
windowDestroyed = true;
latestGenerationRequestId++;
generationQueued = false;
queuedGenerationWasManual = false;
previewWidget?.SetLoading( false );
SavePbrPreferences();
SaveSplitterState();
DisposeBitmaps();
if ( Current == this )
Current = null;
base.OnDestroyed();
}
public override void OnDragHover( Widget.DragEvent ev )
{
var files = SeamlessSuiteImageUtility.GetFilesFromDragData( ev.Data );
ev.Action = SeamlessSuiteImageUtility.FindFirstSupportedFile( files ) != null ? DropAction.Copy : DropAction.Ignore;
}
public override void OnDragDrop( Widget.DragEvent ev )
{
var files = SeamlessSuiteImageUtility.GetFilesFromDragData( ev.Data );
if ( files.Count == 0 )
{
ev.Action = DropAction.Ignore;
return;
}
ev.Action = DropAction.Copy;
LoadDroppedFiles( files.ToArray() );
}
[EditorEvent.Frame]
public void Frame()
{
if ( generationQueued || generationRunning )
previewWidget?.Update();
if ( generationRunning )
return;
if ( !generationQueued || DateTime.Now < queuedGenerationTime )
return;
generationQueued = false;
var manual = queuedGenerationWasManual;
queuedGenerationWasManual = false;
StartGenerateMaps( latestGenerationRequestId, manual );
}
private void BuildLayout()
{
Layout = Layout.Column();
Layout.Margin = 8;
Layout.Spacing = 8;
Layout.Add( BuildToolbar() );
mainSplitter = new Splitter( this );
mainSplitter.IsVertical = true;
mainSplitter.OpaqueResize = true;
mainSplitter.HandleWidth = 5;
Layout.Add( mainSplitter, 1 );
contentSplitter = new Splitter( mainSplitter );
contentSplitter.IsHorizontal = true;
contentSplitter.OpaqueResize = true;
contentSplitter.HandleWidth = 5;
previewSplitter = new Splitter( contentSplitter );
previewSplitter.IsVertical = true;
previewSplitter.OpaqueResize = true;
previewSplitter.HandleWidth = 5;
previewWidget = new PbrPreviewWidget( previewSplitter );
previewWidget.FilesDropped = LoadDroppedFiles;
materialPreviewWidget = new PbrMaterialPreviewWidget( previewSplitter );
materialPreviewWidget.WarningLogged = LogMessage;
materialPreviewWidget.SetPreviewEnabled( experimentalMaterialPreviewEnabled );
materialPreviewWidget.SetParallaxEnabled( parallaxMaterialPreviewEnabled );
materialPreviewWidget.SetPreviewShape( previewShape );
previewSplitter.AddWidget( previewWidget );
previewSplitter.AddWidget( materialPreviewWidget );
previewSplitter.SetStretch( 0, 3 );
previewSplitter.SetStretch( 1, 2 );
previewSplitter.SetCollapsible( 0, false );
previewSplitter.SetCollapsible( 1, true );
ApplyMaterialPreviewPanelState();
inspectorScroll = new ScrollArea( contentSplitter );
inspectorScroll.MinimumWidth = 360;
inspectorScroll.Canvas = BuildInspector( inspectorScroll );
inspectorScroll.Canvas.VerticalSizeMode = SizeMode.CanGrow;
inspectorScroll.Canvas.HorizontalSizeMode = SizeMode.Flexible;
contentSplitter.AddWidget( previewSplitter );
contentSplitter.AddWidget( inspectorScroll );
contentSplitter.SetStretch( 0, 5 );
contentSplitter.SetStretch( 1, 2 );
contentSplitter.SetCollapsible( 0, false );
contentSplitter.SetCollapsible( 1, false );
var console = BuildConsole( mainSplitter );
mainSplitter.AddWidget( contentSplitter );
mainSplitter.AddWidget( console );
mainSplitter.SetStretch( 0, 5 );
mainSplitter.SetStretch( 1, 1 );
mainSplitter.SetCollapsible( 0, false );
mainSplitter.SetCollapsible( 1, false );
RestoreSplitterState();
ApplyMaterialPreviewPanelState();
}
private ToolBar BuildToolbar()
{
var toolbar = new ToolBar( this, "PbrMapGeneratorToolbar" );
toolbar.Movable = false;
toolbar.Floatable = false;
toolbar.SetIconSize( new Vector2( 18, 18 ) );
toolbar.AddOption( "Load Texture", "folder_open", OpenTextureDialog );
toolbar.AddOption( "Link Seamless Output", "link", UseCurrentSuiteTexture );
toolbar.AddOption( "Generate Maps", "auto_fix_high", () => GenerateMaps( true ) );
toolbar.AddOption( "Reset Settings", "refresh", ResetSettings );
toolbar.AddSeparator();
toolbar.AddOption( "Export All", "save", () => ExportAllMaps( false ) );
toolbar.AddOption( "Reveal Output", "folder", RevealOutput );
toolbar.AddSeparator();
toolbar.AddOption( "Clear", "delete", ClearAll );
return toolbar;
}
private Widget BuildInspector( Widget parent )
{
mapButtons.Clear();
var inspector = new Widget( parent );
inspector.MinimumWidth = 340;
inspector.Layout = Layout.Column();
inspector.Layout.Margin = 10;
inspector.Layout.Spacing = 3;
inspector.SetStyles( "background-color: #303030; border-left: 1px solid #202020;" );
AddSectionLabel( inspector, "Source" );
currentFileLabel = AddReadOnlyValueRow( inspector, "Texture", GetSourceFileText(), "The color/albedo texture used to estimate starter PBR maps." );
imageInfoLabel = AddReadOnlyValueRow( inspector, "Info", GetSourceInfoText(), "Shows the loaded texture size and source type." );
noteLabel = AddReadOnlyValueRow( inspector, "Note", GetSourceNoteText(), "PBR maps generated from color are starter estimates." );
AddSectionLabel( inspector, "Preview" );
AddMapButtons( inspector );
AddPreviewLayoutControl( inspector );
if ( experimentalMaterialPreviewEnabled )
AddPreviewShapeControl( inspector );
AddPreviewButtonRow( inspector );
AddMaterialPreviewFeatureControls( inspector );
AddHeightControls( inspector );
AddSectionLabel( inspector, "Normal" );
AddSliderControl( inspector, "Strength", 0f, 12f, 0.1f, settings.NormalStrength, value =>
{
settings.NormalStrength = value;
QueueGenerate();
}, "Controls how strongly height slopes affect the normal map." );
AddSliderControl( inspector, "Smoothing", 0f, 8f, 0.1f, settings.NormalSmoothing, value =>
{
settings.NormalSmoothing = value;
QueueGenerate();
}, "Smooths the height source before normal calculation." );
AddCheckboxRow( inspector, "Flip Green", settings.FlipNormalY, state =>
{
settings.FlipNormalY = state == CheckState.On;
QueueGenerate();
}, "Flips the normal map Y/green channel for workflows that expect the opposite direction." );
AddSectionLabel( inspector, "Roughness" );
AddSliderControl( inspector, "Base", 0f, 1f, 0.01f, settings.RoughnessBase, value =>
{
settings.RoughnessBase = value;
QueueGenerate();
}, "Sets the average roughness value before detail variation." );
AddSliderControl( inspector, "Contrast", 0f, 1f, 0.01f, settings.RoughnessContrast, value =>
{
settings.RoughnessContrast = value;
QueueGenerate();
}, "Controls how much fine image detail changes roughness." );
AddSliderControl( inspector, "Black Point", 0f, 1f, 0.01f, settings.RoughnessBlackPoint, value =>
{
settings.RoughnessBlackPoint = value;
QueueGenerate();
}, "Pushes darker roughness values darker so glossy areas can read more clearly." );
AddSliderControl( inspector, "White Point", 0f, 1f, 0.01f, settings.RoughnessWhitePoint, value =>
{
settings.RoughnessWhitePoint = value;
QueueGenerate();
}, "Pushes brighter roughness values brighter so matte areas can read more clearly." );
AddSliderControl( inspector, "Cavity Roughness", 0f, 1f, 0.01f, settings.RoughnessCavity, value =>
{
settings.RoughnessCavity = value;
QueueGenerate();
}, "Pushes darker height cavities toward white/rougher roughness." );
AddSliderControl( inspector, "Variation", 0f, 1f, 0.01f, settings.RoughnessVariation, value =>
{
settings.RoughnessVariation = value;
QueueGenerate();
}, "Adds subtle grunge/noise breakup so roughness does not feel too flat or plastic." );
AddCheckboxRow( inspector, "Invert", settings.InvertRoughness, state =>
{
settings.InvertRoughness = state == CheckState.On;
QueueGenerate();
}, "Inverts the roughness result." );
AddSectionLabel( inspector, "Ambient Occlusion" );
AddSliderControl( inspector, "Strength", 0f, 2f, 0.05f, settings.AoStrength, value =>
{
settings.AoStrength = value;
QueueGenerate();
}, "Controls how much nearby height difference darkens cavities." );
AddSliderControl( inspector, "Radius", 1f, 32f, 1f, settings.AoRadius, value =>
{
settings.AoRadius = value;
QueueGenerate();
}, "Controls how far the fake cavity AO samples from each pixel." );
AddSliderControl( inspector, "Minimum", 0f, 1f, 0.01f, settings.AoMinimum, value =>
{
settings.AoMinimum = value;
QueueGenerate();
}, "Prevents AO from becoming too dark." );
AddSectionLabel( inspector, "Metallic" );
AddSliderControl( inspector, "Value", 0f, 1f, 0.01f, settings.MetallicValue, value =>
{
settings.MetallicValue = value;
QueueGenerate();
}, "Sets the flat metallic map value. Default is 0 for non-metal starter materials." );
AddSectionLabel( inspector, "Export" );
AddOutputFolderControl( inspector, "Folder where generated PBR maps will be written." );
AddCheckboxRow( inspector, "Overwrite Existing", settings.OverwriteExisting, state =>
{
settings.OverwriteExisting = state == CheckState.On;
}, "Allows export to replace existing files with the same names." );
AddExportButtonRows( inspector );
inspector.Layout.AddStretchCell();
return inspector;
}
private void AddHeightControls( Widget inspector )
{
AddSectionLabel( inspector, "Height" );
AddCheckboxRow( inspector, "Advanced Processing", settings.AdvancedHeightEnabled, state =>
{
settings.AdvancedHeightEnabled = state == CheckState.On;
RebuildInspector();
QueueGenerate();
}, "Enables more advanced height map generation algorithm with fine detail control. Allows more control over the height map details." );
if ( settings.AdvancedHeightEnabled )
{
AddSliderControl( inspector, "Fine Detail", 0f, 4f, 0.05f, settings.AdvancedHeightFineStrength, value =>
{
settings.AdvancedHeightFineStrength = value;
QueueGenerate();
}, "Affects tiny scratches, pores, grit, and sharp little edges." );
AddSliderControl( inspector, "Medium Detail", 0f, 4f, 0.05f, settings.AdvancedHeightMediumStrength, value =>
{
settings.AdvancedHeightMediumStrength = value;
QueueGenerate();
}, "Affects cracks, chips, seams, and other mid-size marks." );
AddSliderControl( inspector, "Large Shape", 0f, 4f, 0.05f, settings.AdvancedHeightLargeStrength, value =>
{
settings.AdvancedHeightLargeStrength = value;
QueueGenerate();
}, "Affects bigger stones, tiles, boards, folds, or large surface shapes." );
AddSliderControl( inspector, "Broad Shape", 0f, 4f, 0.05f, settings.AdvancedHeightBroadStrength, value =>
{
settings.AdvancedHeightBroadStrength = value;
QueueGenerate();
}, "Affects the widest raised and recessed areas across the texture." );
AddSliderControl( inspector, "Scale Radius", 1f, 8f, 0.1f, settings.AdvancedHeightScaleRadius, value =>
{
settings.AdvancedHeightScaleRadius = value;
QueueGenerate();
}, "Changes what size of texture feature counts as fine, medium, large, or broad." );
AddSliderControl( inspector, "Final Contrast", 0f, 3f, 0.05f, settings.AdvancedHeightFinalContrast, value =>
{
settings.AdvancedHeightFinalContrast = value;
QueueGenerate();
}, "Makes raised and recessed parts stand farther apart or closer together." );
AddSliderControl( inspector, "Bias", -1f, 1f, 0.01f, settings.AdvancedHeightBias, value =>
{
settings.AdvancedHeightBias = value;
QueueGenerate();
}, "Moves the whole height map up or down, making the surface overall brighter or darker." );
AddSliderControl( inspector, "Gain", 0f, 4f, 0.05f, settings.AdvancedHeightGain, value =>
{
settings.AdvancedHeightGain = value;
QueueGenerate();
}, "Strengthens or weakens the overall raised and recessed effect." );
AddSliderControl( inspector, "Smoothing", 0f, 16f, 0.1f, settings.AdvancedHeightSmoothing, value =>
{
settings.AdvancedHeightSmoothing = value;
QueueGenerate();
}, "Softens/blurs the height map." );
AddCheckboxRow( inspector, "Invert Height", settings.InvertHeight, state =>
{
settings.InvertHeight = state == CheckState.On;
QueueGenerate();
}, "Inverts the height map, dark pixels as higher and bright pixels as lower." );
return;
}
AddSliderControl( inspector, "Strength", 0f, 4f, 0.05f, settings.HeightStrength, value =>
{
settings.HeightStrength = value;
QueueGenerate();
}, "Controls how strongly luminance is pushed into height variation." );
AddSliderControl( inspector, "Contrast", 0f, 3f, 0.05f, settings.HeightContrast, value =>
{
settings.HeightContrast = value;
QueueGenerate();
}, "Raises or lowers height separation before normal and AO generation." );
AddSliderControl( inspector, "Black Point", 0f, 1f, 0.01f, settings.HeightBlackPoint, value =>
{
settings.HeightBlackPoint = value;
QueueGenerate();
}, "Remaps darker pixels to the bottom of the height range." );
AddSliderControl( inspector, "White Point", 0f, 1f, 0.01f, settings.HeightWhitePoint, value =>
{
settings.HeightWhitePoint = value;
QueueGenerate();
}, "Remaps brighter pixels to the top of the height range." );
AddSliderControl( inspector, "Midpoint", 0f, 1f, 0.01f, settings.HeightMidpoint, value =>
{
settings.HeightMidpoint = value;
QueueGenerate();
}, "Shifts which height value behaves like the neutral middle." );
AddSliderControl( inspector, "Cavity Emphasis", 0f, 1f, 0.01f, settings.HeightCavityEmphasis, value =>
{
settings.HeightCavityEmphasis = value;
QueueGenerate();
}, "Deepens recessed dark height areas before normal and AO generation." );
AddSliderControl( inspector, "Blur", 0f, 16f, 0.1f, settings.HeightBlur, value =>
{
settings.HeightBlur = value;
QueueGenerate();
}, "Softens noisy height detail before dependent maps are generated." );
AddCheckboxRow( inspector, "Invert Height", settings.InvertHeight, state =>
{
settings.InvertHeight = state == CheckState.On;
QueueGenerate();
}, "Treats dark pixels as higher and bright pixels as lower." );
}
private Widget BuildConsole( Widget parent )
{
var pane = new Widget( parent );
pane.Name = "Console";
pane.WindowTitle = "Console";
pane.SetWindowIcon( "terminal" );
pane.MinimumSize = new Vector2( 520, 140 );
pane.Layout = Layout.Column();
pane.Layout.Margin = 8;
pane.Layout.Spacing = 6;
pane.SetStyles( "background-color: #101215; border: 1px solid #2a3038; border-radius: 6px;" );
var title = pane.Layout.Add( new Label( "Console", pane ) );
title.SetStyles( "font-weight: bold; color: #d9dee7;" );
consoleOutput = pane.Layout.Add( new TextEdit( pane ), 1 );
consoleOutput.ReadOnly = true;
consoleOutput.TextSelectable = true;
consoleOutput.BackgroundVisible = false;
consoleOutput.MaximumBlockCount = 200;
consoleOutput.SetStyles( "font-family: monospace; font-size: 11px; color: #b8c0cc;" );
return pane;
}
private void AddSectionLabel( Widget inspector, string text )
{
var row = new Widget( inspector );
row.MinimumHeight = 24;
row.Layout = Layout.Row();
row.Layout.Spacing = 6;
row.SetStyles( "background-color: #2b2b2b; border-top: 1px solid #202020; border-bottom: 1px solid #1f1f1f; margin-top: 6px;" );
var marker = row.Layout.Add( new Label( "-", row ) );
marker.FixedWidth = 20;
marker.SetStyles( "color: #8b8b8b; font-weight: bold; font-size: 12px;" );
var label = row.Layout.Add( new Label( text, row ), 1 );
label.SetStyles( "font-size: 11px; font-weight: bold; color: #ffffff; padding-left: 2px;" );
inspector.Layout.Add( row );
}
private void AddMapButtons( Widget inspector )
{
AddMapButtonRow( inspector, PbrMapType.Albedo, PbrMapType.Height, PbrMapType.Normal, PbrMapType.Roughness );
AddMapButtonRow( inspector, PbrMapType.AmbientOcclusion, PbrMapType.Metallic, PbrMapType.Orm );
}
private void AddMapButtonRow( Widget inspector, params PbrMapType[] mapTypes )
{
var row = new Widget( inspector );
row.Layout = Layout.Row();
row.Layout.Spacing = 5;
row.MinimumHeight = 28;
foreach ( var mapType in mapTypes )
{
var button = row.Layout.Add( new Button( PbrGenerator.GetMapLabel( mapType ), "", row ), 1 );
button.Clicked = () =>
{
selectedMap = mapType;
previewLayout = PbrPreviewLayout.Single;
UpdateMapButtons();
UpdatePreviewSettings();
SavePbrPreferences();
RebuildInspector();
};
mapButtons[mapType] = button;
StyleMapButton( button, mapType == selectedMap );
}
inspector.Layout.Add( row );
}
private void AddPreviewLayoutControl( Widget inspector )
{
var combo = AddComboRow( inspector, "View", "Chooses single-map, tiled repeat, or atlas preview." );
foreach ( var layout in PreviewLayoutComboOrder )
{
combo.AddItem( GetPreviewLayoutLabel( layout ) );
}
combo.CurrentIndex = GetPreviewLayoutComboIndex( previewLayout );
combo.ItemChanged += () =>
{
var comboIndex = Math.Clamp( combo.CurrentIndex, 0, PreviewLayoutComboOrder.Length - 1 );
previewLayout = PreviewLayoutComboOrder[comboIndex];
UpdatePreviewSettings();
SavePbrPreferences();
};
}
private static string GetPreviewLayoutLabel( PbrPreviewLayout layout )
{
return layout switch
{
PbrPreviewLayout.Atlas => "Atlas",
PbrPreviewLayout.Single => "Single",
PbrPreviewLayout.Tiled2X2 => "2 x 2 Tiled",
PbrPreviewLayout.Tiled3X3 => "3 x 3 Tiled",
_ => layout.ToString()
};
}
private static int GetPreviewLayoutComboIndex( PbrPreviewLayout layout )
{
for ( var i = 0; i < PreviewLayoutComboOrder.Length; i++ )
{
if ( PreviewLayoutComboOrder[i] == layout )
{
return i;
}
}
return 0;
}
private void AddPreviewShapeControl( Widget inspector )
{
var combo = AddComboRow( inspector, "Shape", "Chooses the built-in model used by the live material preview." );
combo.AddItem( "Sphere" );
combo.AddItem( "Cube" );
combo.AddItem( "Plane" );
combo.CurrentIndex = (int)previewShape;
combo.ItemChanged += () =>
{
previewShape = (PbrPreviewShape)Math.Clamp( combo.CurrentIndex, 0, 2 );
if ( experimentalMaterialPreviewEnabled )
materialPreviewWidget?.SetPreviewShape( previewShape );
SavePbrPreferences();
};
}
private void AddMaterialPreviewFeatureControls( Widget inspector )
{
AddExperimentalCheckboxRow( inspector, "3D Preview", experimentalMaterialPreviewEnabled, state =>
{
var enabled = state == CheckState.On;
if ( experimentalMaterialPreviewEnabled == enabled )
return;
experimentalMaterialPreviewEnabled = enabled;
materialPreviewWidget?.SetPreviewEnabled( experimentalMaterialPreviewEnabled );
ApplyMaterialPreviewPanelState();
if ( experimentalMaterialPreviewEnabled )
{
LogMessage( "Experimental 3D preview enabled. Preview material generation can be slow, unstable and error-prone." );
UpdateMaterialPreview( true );
}
else
{
LogMessage( "Experimental 3D preview disabled. Preview scene and temporary material cache were cleared." );
}
SavePbrPreferences();
RebuildInspector();
}, "Experimental: enables live 3D material preview. This can write temporary preview assets and may be slow, unstable and error-prone. USE AT YOUR OWN RISK" );
if ( !experimentalMaterialPreviewEnabled )
return;
AddExperimentalCheckboxRow( inspector, "Auto Refresh 3D", autoRefreshMaterialPreview, state =>
{
autoRefreshMaterialPreview = state == CheckState.On;
if ( autoRefreshMaterialPreview )
UpdateMaterialPreview( true );
SavePbrPreferences();
}, "Experimental: automatically rebuilds the 3D preview when generated maps change." );
AddExperimentalCheckboxRow( inspector, "Parallax Preview", parallaxMaterialPreviewEnabled, state =>
{
parallaxMaterialPreviewEnabled = state == CheckState.On;
materialPreviewWidget?.SetParallaxEnabled( parallaxMaterialPreviewEnabled );
UpdateMaterialPreview( true );
SavePbrPreferences();
}, "Experimental: enables complex.shader parallax occlusion in the 3D preview using the generated height map." );
}
private void AddPreviewButtonRow( Widget inspector )
{
var row = new Widget( inspector );
row.Layout = Layout.Row();
row.Layout.Spacing = 10;
row.MinimumHeight = 30;
row.SetStyles( "padding-top: 4px; padding-bottom: 2px;" );
var spacer = row.Layout.Add( new Label( "", row ) );
spacer.FixedWidth = InspectorLabelWidth;
var fitButton = row.Layout.Add( new Button( "Fit", "fit_screen", row ) );
fitButton.MinimumWidth = 86;
fitButton.Clicked = () => previewWidget.FitToView();
StyleInspectorButton( fitButton );
var oneToOneButton = row.Layout.Add( new Button( "1:1", "center_focus_strong", row ) );
oneToOneButton.MinimumWidth = 86;
oneToOneButton.Clicked = () => previewWidget.SetOneToOne();
StyleInspectorButton( oneToOneButton );
row.Layout.AddStretchCell();
inspector.Layout.Add( row );
if ( !experimentalMaterialPreviewEnabled )
return;
var refreshRow = new Widget( inspector );
refreshRow.Layout = Layout.Row();
refreshRow.Layout.Spacing = 10;
var refreshSpacer = refreshRow.Layout.Add( new Label( "", refreshRow ) );
refreshSpacer.FixedWidth = InspectorLabelWidth;
var refreshButton = refreshRow.Layout.Add( new Button( "Refresh 3D Preview", "refresh", refreshRow ), 1 );
refreshButton.Clicked = () => UpdateMaterialPreview( true );
StyleInspectorButton( refreshButton );
SetInspectorToolTip( "Manually rebuilds the experimental 3D preview material from the current generated maps.", refreshButton );
inspector.Layout.Add( refreshRow );
var resetRow = new Widget( inspector );
resetRow.Layout = Layout.Row();
resetRow.Layout.Spacing = 10;
resetRow.MinimumHeight = 30;
resetRow.SetStyles( "padding-top: 0px; padding-bottom: 2px;" );
var resetSpacer = resetRow.Layout.Add( new Label( "", resetRow ) );
resetSpacer.FixedWidth = InspectorLabelWidth;
var resetButton = resetRow.Layout.Add( new Button( "Reset View", "restart_alt", resetRow ), 1 );
resetButton.MinimumWidth = 178;
resetButton.Clicked = () => materialPreviewWidget?.ResetView();
StyleInspectorButton( resetButton );
resetRow.Layout.AddStretchCell();
inspector.Layout.Add( resetRow );
}
private ComboBox AddComboRow( Widget inspector, string labelText, string description = null )
{
var row = new Widget( inspector );
row.Layout = Layout.Row();
row.Layout.Spacing = 6;
row.MinimumHeight = 24;
var label = row.Layout.Add( new Label( labelText, row ) );
StylePropertyLabel( label );
var combo = row.Layout.Add( new ComboBox( row ), 1 );
StyleInputWidget( combo );
SetInspectorToolTip( description, label );
inspector.Layout.Add( row );
return combo;
}
private Widget AddSliderControl( Widget inspector, string labelText, float min, float max, float step, float initialValue, Action<float> changed, string description = null )
{
var row = new Widget( inspector );
row.Layout = Layout.Row();
row.Layout.Spacing = 6;
row.MinimumHeight = 24;
var label = row.Layout.Add( new Label( labelText, row ) );
StylePropertyLabel( label );
var valueBox = row.Layout.Add( new Widget( row ), 1 );
valueBox.Layout = Layout.Row();
valueBox.Layout.Spacing = 5;
valueBox.SetStyles( "background-color: #171717; border: 1px solid #252525; border-radius: 3px;" );
var typeBadge = valueBox.Layout.Add( new Label( "f", valueBox ) );
typeBadge.FixedWidth = 20;
typeBadge.SetStyles( "background-color: #26361d; color: #91c65c; font-weight: bold; font-size: 11px; padding-left: 6px;" );
var slider = valueBox.Layout.Add( new FloatSlider( valueBox ), 1 );
slider.Minimum = min;
slider.Maximum = max;
slider.Step = step;
slider.Value = initialValue;
slider.HighlightColor = new Color( 0.55f, 0.78f, 0.28f, 1f );
slider.MinimumHeight = 22;
var valueLabel = valueBox.Layout.Add( new Label( FormatSliderValue( initialValue, step ), valueBox ) );
valueLabel.FixedWidth = 48;
valueLabel.SetStyles( "color: #f0f0f0; font-size: 11px; padding-left: 4px;" );
slider.OnValueEdited = () =>
{
var snapped = SnapSliderValue( slider.Value, step, min, max );
slider.Value = snapped;
valueLabel.Text = FormatSliderValue( snapped, step );
changed?.Invoke( snapped );
};
SetInspectorToolTip( description, label );
inspector.Layout.Add( row );
return row;
}
private void AddCheckboxRow( Widget inspector, string labelText, bool initialValue, Action<CheckState> changed, string description = null )
{
var row = new Widget( inspector );
row.Layout = Layout.Row();
row.Layout.Spacing = 6;
row.MinimumHeight = 24;
var label = row.Layout.Add( new Label( labelText, row ) );
StylePropertyLabel( label );
var checkbox = row.Layout.Add( new Checkbox( row ) );
checkbox.Value = initialValue;
checkbox.MinimumWidth = 24;
checkbox.StateChanged += changed;
SetInspectorToolTip( description, label );
row.Layout.AddStretchCell();
inspector.Layout.Add( row );
}
private void AddExperimentalCheckboxRow( Widget inspector, string labelText, bool initialValue, Action<CheckState> changed, string description = null )
{
var row = new Widget( inspector );
row.Layout = Layout.Row();
row.Layout.Spacing = 4;
row.MinimumHeight = 26;
var label = row.Layout.Add( new Label( labelText, row ) );
StylePropertyLabel( label );
var checkbox = row.Layout.Add( new Checkbox( row ) );
checkbox.Value = initialValue;
checkbox.FixedWidth = 18;
checkbox.StateChanged += changed;
var badge = row.Layout.Add( new ExperimentalBadgeWidget( row ) );
SetExperimentalToolTip( description, badge );
row.Layout.AddStretchCell();
inspector.Layout.Add( row );
}
private void AddOutputFolderControl( Widget inspector, string description = null )
{
var row = new Widget( inspector );
row.Layout = Layout.Row();
row.Layout.Spacing = 6;
row.MinimumHeight = 24;
var label = row.Layout.Add( new Label( "Output Folder", row ) );
StylePropertyLabel( label );
outputFolderEdit = row.Layout.Add( new LineEdit( row ), 1 );
outputFolderEdit.Text = outputFolder;
outputFolderEdit.PlaceholderText = "Uses the source texture folder";
outputFolderEdit.ClearButtonEnabled = true;
StyleInputWidget( outputFolderEdit );
outputFolderEdit.TextEdited += text =>
{
outputFolder = text;
outputFolderFollowsSource = string.IsNullOrWhiteSpace( outputFolder );
SavePbrPreferences();
};
SetInspectorToolTip( description, label );
inspector.Layout.Add( row );
var buttonRow = new Widget( inspector );
buttonRow.Layout = Layout.Row();
buttonRow.Layout.Spacing = 10;
buttonRow.MinimumHeight = 30;
buttonRow.SetStyles( "padding-top: 4px; padding-bottom: 2px;" );
var spacer = buttonRow.Layout.Add( new Label( "", buttonRow ) );
spacer.FixedWidth = InspectorLabelWidth;
var sourceButton = buttonRow.Layout.Add( new Button( "Use Source", "source", buttonRow ) );
sourceButton.MinimumWidth = 104;
sourceButton.Clicked = UseSourceFolder;
StyleInspectorButton( sourceButton );
var browseButton = buttonRow.Layout.Add( new Button( "Browse", "folder_open", buttonRow ) );
browseButton.MinimumWidth = 88;
browseButton.Clicked = BrowseOutputFolder;
StyleInspectorButton( browseButton );
buttonRow.Layout.AddStretchCell();
inspector.Layout.Add( buttonRow );
}
private void AddExportButtonRows( Widget inspector )
{
AddExportButtonRow( inspector, "Export Current Map", "save", ExportCurrentMap, "Exports only the map you are previewing right now." );
AddExportButtonRow( inspector, "Export ORM Only", "save", ExportOrmOnly, "Exports the packed AO, Roughness, and Metallic texture for material workflows that use one ORM map." );
AddExportButtonRow( inspector, "Export All Maps", "save", () => ExportAllMaps( false ), "Exports the full starter stack: albedo, height, normal, roughness, AO, metallic, and ORM." );
AddExportButtonRow( inspector, "Export All + VMAT", "inventory_2", () => ExportAllMaps( true ), "Exports the full stack and creates a VMAT using the individual material texture slots." );
}
private void AddExportButtonRow( Widget inspector, string text, string icon, Action clicked, string description )
{
var row = new Widget( inspector );
row.Layout = Layout.Row();
row.Layout.Spacing = 8;
row.MinimumHeight = 34;
row.SetStyles( "padding-top: 4px;" );
row.Layout.AddStretchCell();
var button = row.Layout.Add( new Button( text, icon, row ) );
button.MinimumWidth = 250;
button.MinimumHeight = 28;
button.Clicked = clicked;
StyleExportButton( button );
button.ToolTip = $"<html><body style=\"background-color:#252525; color:#ffffff; white-space: nowrap;\">{description}</body></html>";
row.Layout.AddStretchCell();
inspector.Layout.Add( row );
}
private Label AddReadOnlyValueRow( Widget inspector, string labelText, string value, string description = null )
{
var row = new Widget( inspector );
row.Layout = Layout.Row();
row.Layout.Spacing = 6;
row.MinimumHeight = 24;
var label = row.Layout.Add( new Label( labelText, row ) );
StylePropertyLabel( label );
var valueLabel = row.Layout.Add( new Label( value, row ), 1 );
valueLabel.WordWrap = true;
valueLabel.SetStyles( "background-color: #171717; border: 1px solid #252525; border-radius: 3px; color: #f0f0f0; font-size: 11px; padding-left: 8px; padding-top: 3px;" );
SetInspectorToolTip( description, label );
inspector.Layout.Add( row );
return valueLabel;
}
private void SetInspectorToolTip( string description, params Widget[] widgets )
{
if ( string.IsNullOrWhiteSpace( description ) )
return;
foreach ( var widget in widgets )
{
if ( widget != null )
widget.ToolTip = description;
}
}
private void SetExperimentalToolTip( string description, Widget widget )
{
if ( string.IsNullOrWhiteSpace( description ) || widget == null )
return;
widget.ToolTip = $"<span style=\"color:#ff5f5f; font-weight:bold;\">{description}</span>";
}
private void StylePropertyLabel( Label label )
{
label.FixedWidth = InspectorLabelWidth;
label.SetStyles( "color: #d6d6d6; font-size: 11px; padding-top: 4px; padding-left: 10px;" );
}
private void StyleInputWidget( Widget widget )
{
widget.MinimumHeight = 22;
widget.SetStyles( "background-color: #171717; border: 1px solid #252525; border-radius: 3px; color: #f0f0f0; font-size: 11px;" );
}
private void StyleInspectorButton( Button button )
{
button.MinimumHeight = 22;
button.SetStyles( "background-color: #1d1d1d; border: 1px solid #292929; border-radius: 3px; color: #e6e6e6; font-size: 11px;" );
}
private void StyleExportButton( Button button )
{
button.MinimumHeight = 28;
button.Tint = new Color( 0.29f, 0.30f, 0.32f );
button.SetStyles( "color: #ffffff; font-size: 12px; font-weight: bold;" );
}
private void StyleMapButton( Button button, bool selected )
{
if ( selected )
{
button.SetStyles( "background-color: #467022; border: 1px solid #6d9d36; border-radius: 4px; color: #ffffff; font-size: 11px; font-weight: bold;" );
return;
}
button.SetStyles( "background-color: #1d1d1d; border: 1px solid #292929; border-radius: 4px; color: #d6d6d6; font-size: 11px;" );
}
private sealed class ExperimentalBadgeWidget : Widget
{
public ExperimentalBadgeWidget( Widget parent ) : base( parent )
{
FixedWidth = 96f;
MinimumHeight = 22f;
}
protected override void OnPaint()
{
var center = new Vector2( 6f, LocalRect.Center.y );
Paint.ClearPen();
Paint.SetBrush( new Color( 0.42f, 0.11f, 0.11f ) );
Paint.DrawCircle( new Rect( center.x - 5f, center.y - 5f, 10f, 10f ) );
Paint.SetDefaultFont( 8, 700, false, false );
Paint.SetPen( new Color( 1f, 0.62f, 0.62f ) );
Paint.DrawText( new Rect( 0f, 1f, 12f, Height ), "i", TextFlag.Center );
Paint.SetDefaultFont( 9, 700, false, false );
Paint.SetPen( new Color( 1f, 0.36f, 0.36f ) );
Paint.DrawText( new Rect( 14f, 0f, Width - 14f, Height ), "Experimental", TextFlag.LeftCenter );
}
}
private void UpdateMapButtons()
{
foreach ( var pair in mapButtons )
{
StyleMapButton( pair.Value, pair.Key == selectedMap );
}
}
private void OpenTextureDialog()
{
var dialog = new FileDialog( this )
{
Title = "Open Albedo Texture",
DefaultSuffix = ".png"
};
dialog.SetModeOpen();
dialog.SetFindExistingFile();
dialog.SetNameFilter( "Images (*.png *.jpg *.jpeg *.webp *.bmp *.tga *.tif *.tiff *.psd *.svg);;All Files (*.*)" );
if ( !dialog.Execute() )
return;
LoadTexture( dialog.SelectedFile );
}
private void LoadDroppedFiles( string[] droppedFiles )
{
var files = new List<string>();
foreach ( var file in droppedFiles )
{
files.Add( file );
}
var supportedFile = SeamlessSuiteImageUtility.FindFirstSupportedFile( files );
if ( supportedFile == null )
{
LogMessage( "Drop ignored. No supported image file was found." );
ShowStatus( "No supported image file found", 4f );
return;
}
LoadTexture( supportedFile );
var skippedCount = Math.Max( 0, files.Count - 1 );
if ( skippedCount > 0 )
LogMessage( $"One-texture mode loaded the first supported image and skipped {skippedCount} other file(s)." );
}
private void LoadTexture( string path )
{
var resolvedPath = SeamlessSuiteImageUtility.ResolveFilePath( path );
if ( string.IsNullOrWhiteSpace( resolvedPath ) || !File.Exists( resolvedPath ) )
{
LogMessage( $"Could not find texture: {path}" );
ShowStatus( "Texture file not found", 4f );
return;
}
if ( !SeamlessSuiteImageUtility.IsSupportedImageFile( resolvedPath ) )
{
LogMessage( $"Unsupported image type: {Path.GetExtension( resolvedPath )}" );
ShowStatus( "Unsupported image type", 4f );
return;
}
try
{
var bitmap = SeamlessSuiteImageUtility.LoadBitmapFromFile( resolvedPath );
if ( bitmap == null || !bitmap.IsValid )
{
bitmap?.Dispose();
LogMessage( $"S&box could not decode texture: {Path.GetFileName( resolvedPath )}" );
ShowStatus( "Texture decode failed", 4f );
return;
}
SetSourceTexture( bitmap, resolvedPath, Path.GetFileNameWithoutExtension( resolvedPath ) );
LogMessage( $"Loaded {resolvedPath}" );
ShowStatus( "Texture loaded", 4f );
GenerateMaps( false );
}
catch ( Exception ex )
{
LogMessage( $"Load failed: {ex.Message}" );
ShowStatus( "Load failed", 4f );
}
}
private void UseCurrentSuiteTexture()
{
var bitmap = SeamlessSuiteActiveTexture.GetClone();
if ( bitmap == null || !bitmap.IsValid )
{
bitmap?.Dispose();
LogMessage( "No current suite texture is available. Process a texture in the Seam-Less™ tab first." );
ShowStatus( "No current suite texture", 4f );
return;
}
var displayName = SeamlessSuiteActiveTexture.DisplayName ?? "Current Suite Texture";
SetSourceTexture( bitmap, SeamlessSuiteActiveTexture.SourcePath, displayName );
LogMessage( $"Using current suite texture: {displayName}" );
ShowStatus( "Current suite texture loaded", 4f );
GenerateMaps( false );
}
private void SetSourceTexture( Bitmap bitmap, string path, string displayName )
{
sourceBitmap?.Dispose();
generatedResult?.Dispose();
sourceBitmap = bitmap;
generatedResult = null;
sourceImagePath = path;
sourceDisplayName = displayName;
previewWidget?.SetResult( null );
previewWidget?.SetBitmap( null );
if ( experimentalMaterialPreviewEnabled )
materialPreviewWidget?.SetResult( null );
if ( outputFolderFollowsSource || string.IsNullOrWhiteSpace( outputFolder ) )
SetOutputFolderToSource();
UpdateSourceLabels();
previewWidget.SetCaption( GetPreviewCaption() );
}
private void GenerateMaps( bool manual )
{
if ( sourceBitmap == null || !sourceBitmap.IsValid )
{
if ( manual )
LogMessage( "No source texture loaded. Open, drop, or use the current suite texture first." );
ShowStatus( "No source texture loaded", 4f );
return;
}
latestGenerationRequestId++;
if ( generationRunning )
{
generationQueued = true;
queuedGenerationWasManual |= manual;
queuedGenerationTime = DateTime.Now.AddMilliseconds( 140 );
previewWidget?.SetLoading( true );
return;
}
StartGenerateMaps( latestGenerationRequestId, manual );
}
private void StartGenerateMaps( int requestId, bool manual )
{
if ( sourceBitmap == null || !sourceBitmap.IsValid )
return;
var settingsSnapshot = CreateSettingsSnapshot();
var processStarted = DateTime.Now;
generationRunning = true;
runningGenerationRequestId = requestId;
previewWidget?.SetLoading( true );
Color[] sourcePixels = null;
var width = 0;
var height = 0;
var albedoIsFloatingPoint = false;
try
{
width = sourceBitmap.Width;
height = sourceBitmap.Height;
albedoIsFloatingPoint = sourceBitmap.IsFloatingPoint;
sourcePixels = sourceBitmap.GetPixels();
}
catch ( Exception ex )
{
generationRunning = false;
runningGenerationRequestId = 0;
LogMessage( $"Generation failed before it could start: {ex.Message}" );
ShowStatus( "Generation failed", 4f );
previewWidget?.SetLoading( false );
return;
}
_ = Task.Run( () =>
{
PbrGeneratorPixelResult pixelResult = null;
Exception error = null;
try
{
pixelResult = PbrGenerator.GeneratePixels( sourcePixels, width, height, albedoIsFloatingPoint, settingsSnapshot );
}
catch ( Exception ex )
{
error = ex;
}
MainThread.Queue( () => FinishGenerateMaps( requestId, manual, processStarted, pixelResult, error ) );
} );
}
private void FinishGenerateMaps( int requestId, bool manual, DateTime processStarted, PbrGeneratorPixelResult pixelResult, Exception error )
{
if ( error != null )
{
FinishGenerateMaps( requestId, manual, processStarted, (PbrGeneratorResult)null, error );
return;
}
if ( !CanAcceptGenerationResult( requestId ) )
{
generationRunning = false;
runningGenerationRequestId = 0;
StartQueuedGenerationIfReady();
return;
}
var result = CreateGeneratedBitmapResult( pixelResult );
FinishGenerateMaps( requestId, manual, processStarted, result, null );
}
private void FinishGenerateMaps( int requestId, bool manual, DateTime processStarted, PbrGeneratorResult result, Exception error )
{
var wasRunningRequest = requestId == runningGenerationRequestId;
generationRunning = false;
runningGenerationRequestId = 0;
if ( windowDestroyed || !wasRunningRequest || requestId != latestGenerationRequestId || generationQueued )
{
result?.Dispose();
StartQueuedGenerationIfReady();
return;
}
if ( error != null )
{
UpdatePreviewSettings();
LogMessage( $"Generation failed: {error.Message}" );
ShowStatus( "Generation failed", 4f );
previewWidget?.SetLoading( false );
return;
}
if ( result == null || result.Albedo == null || !result.Albedo.IsValid )
{
result?.Dispose();
UpdatePreviewSettings();
LogMessage( "PBR generation failed. The bitmap result was invalid." );
ShowStatus( "Generation failed", 4f );
previewWidget?.SetLoading( false );
return;
}
var oldResult = generatedResult;
generatedResult = result;
oldResult?.Dispose();
UpdatePreviewSettings();
UpdateMaterialPreview( manual );
UpdateSourceLabels();
previewWidget?.SetLoading( false );
if ( manual )
LogMessage( "Generated starter PBR maps with current settings." );
var elapsed = DateTime.Now - processStarted;
if ( elapsed.TotalMilliseconds > 1500 )
LogMessage( $"PBR generation took {elapsed.TotalSeconds:0.0}s. Large textures can take a moment." );
ShowStatus( "PBR maps updated", 2f );
}
private bool CanAcceptGenerationResult( int requestId )
{
return !windowDestroyed
&& requestId == runningGenerationRequestId
&& requestId == latestGenerationRequestId
&& !generationQueued;
}
private PbrGeneratorResult CreateGeneratedBitmapResult( PbrGeneratorPixelResult pixelResult )
{
try
{
return PbrGenerator.CreateBitmapResult( pixelResult );
}
catch ( Exception ex )
{
LogMessage( $"Generation failed while building preview bitmaps: {ex.Message}" );
return null;
}
}
private void StartQueuedGenerationIfReady()
{
if ( windowDestroyed || generationRunning || !generationQueued )
{
if ( !generationRunning && !generationQueued )
previewWidget?.SetLoading( false );
return;
}
if ( sourceBitmap == null || !sourceBitmap.IsValid )
{
generationQueued = false;
queuedGenerationWasManual = false;
previewWidget?.SetLoading( false );
return;
}
generationQueued = false;
var manual = queuedGenerationWasManual;
queuedGenerationWasManual = false;
StartGenerateMaps( latestGenerationRequestId, manual );
}
private void ResetSettings()
{
settings = new PbrGeneratorSettings();
selectedMap = PbrMapType.Albedo;
previewLayout = PbrPreviewLayout.Atlas;
previewShape = PbrPreviewShape.Sphere;
experimentalMaterialPreviewEnabled = false;
autoRefreshMaterialPreview = false;
parallaxMaterialPreviewEnabled = false;
materialPreviewWidget?.SetParallaxEnabled( parallaxMaterialPreviewEnabled );
materialPreviewWidget?.SetPreviewShape( previewShape );
materialPreviewWidget?.SetPreviewEnabled( experimentalMaterialPreviewEnabled );
ApplyMaterialPreviewPanelState();
SavePbrPreferences();
RebuildInspector();
previewWidget.SetPreviewLayout( previewLayout );
previewWidget.FitToView();
if ( sourceBitmap != null && sourceBitmap.IsValid )
GenerateMaps( false );
else
UpdateMaterialPreview( true );
LogMessage( "PBR settings reset to defaults." );
ShowStatus( "Settings reset", 3f );
}
private void RebuildInspector()
{
if ( inspectorScroll == null )
return;
var oldCanvas = inspectorScroll.Canvas;
inspectorScroll.Canvas = BuildInspector( inspectorScroll );
inspectorScroll.Canvas.VerticalSizeMode = SizeMode.CanGrow;
inspectorScroll.Canvas.HorizontalSizeMode = SizeMode.Flexible;
if ( oldCanvas != null && oldCanvas.IsValid )
{
oldCanvas.DestroyChildren();
oldCanvas.Hidden = true;
}
}
private void QueueGenerate()
{
if ( sourceBitmap == null || !sourceBitmap.IsValid )
return;
latestGenerationRequestId++;
generationQueued = true;
queuedGenerationWasManual = false;
queuedGenerationTime = DateTime.Now.AddMilliseconds( 140 );
previewWidget?.SetLoading( true );
}
private PbrGeneratorSettings CreateSettingsSnapshot()
{
return new PbrGeneratorSettings
{
HeightStrength = settings.HeightStrength,
HeightContrast = settings.HeightContrast,
HeightBlackPoint = settings.HeightBlackPoint,
HeightWhitePoint = settings.HeightWhitePoint,
HeightMidpoint = settings.HeightMidpoint,
HeightCavityEmphasis = settings.HeightCavityEmphasis,
HeightBlur = settings.HeightBlur,
InvertHeight = settings.InvertHeight,
AdvancedHeightEnabled = settings.AdvancedHeightEnabled,
AdvancedHeightFineStrength = settings.AdvancedHeightFineStrength,
AdvancedHeightMediumStrength = settings.AdvancedHeightMediumStrength,
AdvancedHeightLargeStrength = settings.AdvancedHeightLargeStrength,
AdvancedHeightBroadStrength = settings.AdvancedHeightBroadStrength,
AdvancedHeightScaleRadius = settings.AdvancedHeightScaleRadius,
AdvancedHeightFinalContrast = settings.AdvancedHeightFinalContrast,
AdvancedHeightBias = settings.AdvancedHeightBias,
AdvancedHeightGain = settings.AdvancedHeightGain,
AdvancedHeightSmoothing = settings.AdvancedHeightSmoothing,
NormalStrength = settings.NormalStrength,
NormalSmoothing = settings.NormalSmoothing,
FlipNormalY = settings.FlipNormalY,
RoughnessBase = settings.RoughnessBase,
RoughnessContrast = settings.RoughnessContrast,
RoughnessBlackPoint = settings.RoughnessBlackPoint,
RoughnessWhitePoint = settings.RoughnessWhitePoint,
RoughnessCavity = settings.RoughnessCavity,
RoughnessVariation = settings.RoughnessVariation,
InvertRoughness = settings.InvertRoughness,
AoStrength = settings.AoStrength,
AoRadius = settings.AoRadius,
AoMinimum = settings.AoMinimum,
MetallicValue = settings.MetallicValue,
OverwriteExisting = settings.OverwriteExisting
};
}
private void UpdatePreviewSettings()
{
var map = generatedResult?.GetMap( selectedMap );
previewWidget.SetPreviewLayout( previewLayout );
previewWidget.SetResult( generatedResult );
previewWidget.SetBitmap( map );
previewWidget.SetCaption( GetPreviewCaption() );
}
private void UpdateMaterialPreview( bool force = false )
{
if ( materialPreviewWidget == null )
return;
ApplyMaterialPreviewPanelState();
materialPreviewWidget.SetPreviewEnabled( experimentalMaterialPreviewEnabled );
if ( !experimentalMaterialPreviewEnabled )
return;
materialPreviewWidget.SetParallaxEnabled( parallaxMaterialPreviewEnabled );
materialPreviewWidget.SetPreviewShape( previewShape );
if ( !force && !autoRefreshMaterialPreview )
return;
materialPreviewWidget.SetResult( generatedResult );
}
private void ApplyMaterialPreviewPanelState()
{
if ( materialPreviewWidget == null )
return;
materialPreviewWidget.Hidden = !experimentalMaterialPreviewEnabled;
materialPreviewWidget.MinimumHeight = experimentalMaterialPreviewEnabled ? 220f : 0f;
previewSplitter?.SetStretch( 0, experimentalMaterialPreviewEnabled ? 3 : 1 );
previewSplitter?.SetStretch( 1, experimentalMaterialPreviewEnabled ? 2 : 0 );
previewSplitter?.UpdateGeometry();
}
private void UpdateSourceLabels()
{
if ( currentFileLabel != null )
currentFileLabel.Text = GetSourceFileText();
if ( imageInfoLabel != null )
imageInfoLabel.Text = GetSourceInfoText();
if ( noteLabel != null )
noteLabel.Text = GetSourceNoteText();
}
private string GetPreviewCaption()
{
var mapLabel = previewLayout == PbrPreviewLayout.Atlas
? "Atlas"
: PbrGenerator.GetMapLabel( selectedMap );
if ( sourceBitmap == null || !sourceBitmap.IsValid )
return $"{mapLabel} | Drop an albedo texture here or use Load Texture";
return $"{mapLabel} | {GetSourceFileText()} | {sourceBitmap.Width} x {sourceBitmap.Height}";
}
private string GetSourceFileText()
{
if ( !string.IsNullOrWhiteSpace( sourceImagePath ) )
return Path.GetFileName( sourceImagePath );
if ( !string.IsNullOrWhiteSpace( sourceDisplayName ) )
return sourceDisplayName;
return "No texture loaded";
}
private string GetSourceInfoText()
{
if ( sourceBitmap == null || !sourceBitmap.IsValid )
return "Drop an albedo texture into the preview, or use Load Texture.";
var sourceType = !string.IsNullOrWhiteSpace( sourceImagePath )
? Path.GetExtension( sourceImagePath ).TrimStart( '.' ).ToUpperInvariant()
: "SUITE";
return $"{sourceBitmap.Width} x {sourceBitmap.Height} | {sourceType}";
}
private string GetSourceNoteText()
{
if ( sourceBitmap == null || !sourceBitmap.IsValid )
return "PBR maps generated from color are starter estimates.";
var note = "Generated maps are starter estimates from color only.";
if ( sourceBitmap.Width != sourceBitmap.Height )
note += " Input is not square.";
if ( sourceBitmap.Width < 256 || sourceBitmap.Height < 256 )
note += " Input is low resolution.";
return note;
}
private void ExportCurrentMap()
{
if ( !EnsureMapsGenerated() )
return;
try
{
var outputDirectory = GetOutputDirectory();
Directory.CreateDirectory( outputDirectory );
var paths = new PbrExportedMapPaths();
var path = ExportMap( selectedMap, outputDirectory, paths );
if ( string.IsNullOrWhiteSpace( path ) )
return;
lastExportPath = path;
ShowStatus( "Current map export complete", 5f );
}
catch ( Exception ex )
{
LogMessage( $"Export failed: {ex.Message}" );
ShowStatus( "Export failed", 5f );
}
}
private void ExportOrmOnly()
{
if ( !EnsureMapsGenerated() )
return;
try
{
var outputDirectory = GetOutputDirectory();
Directory.CreateDirectory( outputDirectory );
var paths = new PbrExportedMapPaths();
var path = ExportMap( PbrMapType.Orm, outputDirectory, paths );
if ( string.IsNullOrWhiteSpace( path ) )
return;
lastExportPath = path;
ShowStatus( "ORM export complete", 5f );
}
catch ( Exception ex )
{
LogMessage( $"Export failed: {ex.Message}" );
ShowStatus( "Export failed", 5f );
}
}
private void ExportAllMaps( bool includeVmat )
{
if ( !EnsureMapsGenerated() )
return;
try
{
var outputDirectory = GetOutputDirectory();
Directory.CreateDirectory( outputDirectory );
var paths = new PbrExportedMapPaths();
ExportMap( PbrMapType.Albedo, outputDirectory, paths );
ExportMap( PbrMapType.Height, outputDirectory, paths );
ExportMap( PbrMapType.Normal, outputDirectory, paths );
ExportMap( PbrMapType.Roughness, outputDirectory, paths );
ExportMap( PbrMapType.AmbientOcclusion, outputDirectory, paths );
ExportMap( PbrMapType.Metallic, outputDirectory, paths );
ExportMap( PbrMapType.Orm, outputDirectory, paths );
lastExportPath = paths.OrmPath ?? paths.AlbedoPath;
if ( includeVmat )
{
var vmatPath = SeamlessSuiteImageUtility.BuildUniquePath( outputDirectory, GetSourceBaseName(), "_pbr", ".vmat", settings.OverwriteExisting );
var materialResult = PbrMaterialExporter.Export( vmatPath, paths, settings.OverwriteExisting );
if ( !string.IsNullOrWhiteSpace( materialResult.Warning ) )
LogMessage( $"VMAT warning: {materialResult.Warning}" );
if ( materialResult.Success )
{
lastExportPath = materialResult.VmatPath;
LogMessage( $"Exported VMAT {materialResult.VmatPath}" );
ShowStatus( "PBR stack and VMAT export complete", 5f );
return;
}
var error = string.IsNullOrWhiteSpace( materialResult.Error )
? "Unknown error."
: materialResult.Error;
LogMessage( $"VMAT export failed: {error}" );
ShowStatus( "Maps exported; VMAT failed", 5f );
return;
}
ShowStatus( "PBR stack export complete", 5f );
}
catch ( Exception ex )
{
LogMessage( $"Export failed: {ex.Message}" );
ShowStatus( "Export failed", 5f );
}
}
private bool EnsureMapsGenerated()
{
if ( generationQueued || generationRunning )
{
LogMessage( "Maps are still building. Try exporting again when the preview finishes." );
ShowStatus( "Maps are still building", 4f );
return false;
}
if ( generatedResult == null || generatedResult.Albedo == null || !generatedResult.Albedo.IsValid )
{
GenerateMaps( true );
LogMessage( "Maps are generating. Try exporting again when the preview finishes." );
ShowStatus( "Maps are generating", 4f );
}
return generatedResult != null && generatedResult.Albedo != null && generatedResult.Albedo.IsValid;
}
private string ExportMap( PbrMapType mapType, string outputDirectory, PbrExportedMapPaths paths )
{
var bitmap = generatedResult?.GetMap( mapType );
if ( bitmap == null || !bitmap.IsValid )
{
LogMessage( $"Skipped {PbrGenerator.GetMapLabel( mapType )}; generated bitmap was invalid." );
return null;
}
var outputPath = BuildMapOutputPath( outputDirectory, mapType );
File.WriteAllBytes( outputPath, bitmap.ToPng() );
SeamlessSuiteImageUtility.RegisterFile( outputPath, LogMessage );
SetExportedPath( paths, mapType, outputPath );
LogMessage( $"Exported {outputPath}" );
return outputPath;
}
private void SetExportedPath( PbrExportedMapPaths paths, PbrMapType mapType, string outputPath )
{
switch ( mapType )
{
case PbrMapType.Albedo:
paths.AlbedoPath = outputPath;
return;
case PbrMapType.Height:
paths.HeightPath = outputPath;
return;
case PbrMapType.Normal:
paths.NormalPath = outputPath;
return;
case PbrMapType.Roughness:
paths.RoughnessPath = outputPath;
return;
case PbrMapType.AmbientOcclusion:
paths.AoPath = outputPath;
return;
case PbrMapType.Metallic:
paths.MetallicPath = outputPath;
return;
case PbrMapType.Orm:
paths.OrmPath = outputPath;
return;
}
}
private string BuildMapOutputPath( string outputDirectory, PbrMapType mapType )
{
return SeamlessSuiteImageUtility.BuildUniquePath(
outputDirectory,
GetSourceBaseName(),
PbrGenerator.GetMapSuffix( mapType ),
".png",
settings.OverwriteExisting
);
}
private string GetSourceBaseName()
{
if ( !string.IsNullOrWhiteSpace( sourceImagePath ) )
return Path.GetFileNameWithoutExtension( sourceImagePath );
if ( !string.IsNullOrWhiteSpace( sourceDisplayName ) )
return sourceDisplayName;
return "pbr_texture";
}
private string GetOutputDirectory()
{
if ( !string.IsNullOrWhiteSpace( outputFolder ) )
return outputFolder;
if ( !string.IsNullOrWhiteSpace( sourceImagePath ) )
return Path.GetDirectoryName( sourceImagePath ) ?? Directory.GetCurrentDirectory();
return Directory.GetCurrentDirectory();
}
private void RevealOutput()
{
if ( !string.IsNullOrWhiteSpace( lastExportPath ) && File.Exists( lastExportPath ) )
{
EditorUtility.OpenFileFolder( lastExportPath );
return;
}
var outputDirectory = GetOutputDirectory();
if ( Directory.Exists( outputDirectory ) )
{
EditorUtility.OpenFolder( outputDirectory );
return;
}
LogMessage( "No output folder exists yet." );
ShowStatus( "No output folder exists yet", 4f );
}
private void BrowseOutputFolder()
{
var dialog = new FileDialog( this )
{
Title = "Choose Output Folder"
};
dialog.SetModeOpen();
dialog.SetFindDirectory();
if ( !string.IsNullOrWhiteSpace( outputFolder ) )
dialog.Directory = outputFolder;
if ( !dialog.Execute() )
return;
var folder = !string.IsNullOrWhiteSpace( dialog.SelectedFile )
? dialog.SelectedFile
: dialog.Directory;
if ( string.IsNullOrWhiteSpace( folder ) )
return;
outputFolderFollowsSource = false;
outputFolder = folder;
outputFolderEdit.Text = folder;
SavePbrPreferences();
LogMessage( $"Output folder set to {folder}" );
}
private void UseSourceFolder()
{
outputFolderFollowsSource = true;
SetOutputFolderToSource();
SavePbrPreferences();
LogMessage( "Output folder now follows the source texture folder." );
}
private void SetOutputFolderToSource()
{
var sourceFolder = !string.IsNullOrWhiteSpace( sourceImagePath )
? Path.GetDirectoryName( sourceImagePath )
: "";
outputFolder = sourceFolder ?? "";
if ( outputFolderEdit != null )
outputFolderEdit.Text = outputFolder;
}
private void ApplySavedPreferences( PbrGeneratorPreferences preferences )
{
if ( preferences == null )
return;
outputFolderFollowsSource = preferences.OutputFolderFollowsSource;
outputFolder = outputFolderFollowsSource ? "" : preferences.OutputFolder ?? "";
selectedMap = preferences.SelectedMap;
previewLayout = preferences.PreviewLayout;
previewShape = preferences.PreviewShape;
experimentalMaterialPreviewEnabled = preferences.ExperimentalMaterialPreviewEnabled;
autoRefreshMaterialPreview = preferences.AutoRefreshMaterialPreview;
parallaxMaterialPreviewEnabled = preferences.ParallaxMaterialPreviewEnabled;
}
private void SavePbrPreferences()
{
PbrGeneratorPreferences.Save( new PbrGeneratorPreferences
{
OutputFolder = outputFolderFollowsSource ? "" : outputFolder ?? "",
OutputFolderFollowsSource = outputFolderFollowsSource,
SelectedMap = selectedMap,
PreviewLayout = previewLayout,
PreviewShape = previewShape,
ExperimentalMaterialPreviewEnabled = experimentalMaterialPreviewEnabled,
AutoRefreshMaterialPreview = autoRefreshMaterialPreview,
ParallaxMaterialPreviewEnabled = parallaxMaterialPreviewEnabled
} );
}
private void RestoreSplitterState()
{
try
{
if ( EditorCookie.TryGetString( ContentSplitterCookie, out var contentState ) && !string.IsNullOrWhiteSpace( contentState ) )
contentSplitter?.RestoreState( contentState );
if ( EditorCookie.TryGetString( PreviewSplitterCookie, out var previewState ) && !string.IsNullOrWhiteSpace( previewState ) )
previewSplitter?.RestoreState( previewState );
if ( EditorCookie.TryGetString( MainSplitterCookie, out var mainState ) && !string.IsNullOrWhiteSpace( mainState ) )
mainSplitter?.RestoreState( mainState );
}
catch ( Exception ex )
{
LogMessage( $"Could not restore panel sizes, using defaults: {ex.Message}" );
}
}
private void SaveSplitterState()
{
try
{
if ( contentSplitter != null )
EditorCookie.SetString( ContentSplitterCookie, contentSplitter.SaveState() );
if ( previewSplitter != null )
EditorCookie.SetString( PreviewSplitterCookie, previewSplitter.SaveState() );
if ( mainSplitter != null )
EditorCookie.SetString( MainSplitterCookie, mainSplitter.SaveState() );
}
catch ( Exception ex )
{
Log.Info( $"Seam-Less PBR: Could not save panel sizes: {ex.Message}" );
}
}
private float SnapSliderValue( float value, float step, float min, float max )
{
var clamped = Math.Clamp( value, min, max );
if ( step <= 0f )
return clamped;
return Math.Clamp( MathF.Round( clamped / step ) * step, min, max );
}
private string FormatSliderValue( float value, float step )
{
if ( step >= 1f )
return MathF.Round( value ).ToString();
if ( step >= 0.05f )
return value.ToString( "0.0" );
return value.ToString( "0.00" );
}
private void ShowStatus( string message, float seconds )
{
var line = $"[{DateTime.Now:HH:mm:ss}] Status: {message}";
consoleOutput?.AppendPlainText( line );
consoleOutput?.ScrollToBottom();
}
private void LogMessage( string message )
{
var line = $"[{DateTime.Now:HH:mm:ss}] {message}";
consoleOutput?.AppendPlainText( line );
consoleOutput?.ScrollToBottom();
Log.Info( $"Seam-Less PBR: {message}" );
}
private void ClearAll()
{
latestGenerationRequestId++;
generationQueued = false;
queuedGenerationWasManual = false;
previewWidget?.SetLoading( false );
DisposeBitmaps();
sourceImagePath = null;
sourceDisplayName = null;
lastExportPath = null;
if ( outputFolderFollowsSource )
SetOutputFolderToSource();
UpdateSourceLabels();
previewWidget?.SetCaption( "Drop an albedo texture here or use Load Texture" );
previewWidget?.SetResult( null );
previewWidget?.SetBitmap( null );
if ( experimentalMaterialPreviewEnabled )
materialPreviewWidget?.SetResult( null );
consoleOutput?.Clear();
Log.Info( "Seam-Less PBR: Cleared texture and log." );
}
private void DisposeBitmaps()
{
if ( experimentalMaterialPreviewEnabled )
materialPreviewWidget?.SetResult( null );
previewWidget?.SetLoading( false );
previewWidget?.SetResult( null );
sourceBitmap?.Dispose();
generatedResult?.Dispose();
sourceBitmap = null;
generatedResult = null;
}
}