diff --git a/OtterGui b/OtterGui index ebaedd64..2feb762d 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit ebaedd64ed28032e4c9bc34c0c1ec3488b2f0937 +Subproject commit 2feb762d72c8717d8ece6a0bf72298776312b2c1 diff --git a/Penumbra.GameData/Files/MtrlFile.cs b/Penumbra.GameData/Files/MtrlFile.cs index f890f539..c1c683e7 100644 --- a/Penumbra.GameData/Files/MtrlFile.cs +++ b/Penumbra.GameData/Files/MtrlFile.cs @@ -23,8 +23,6 @@ public partial class MtrlFile : IWritable public ShaderPackageData ShaderPackage; public byte[] AdditionalData; - public ShpkFile? AssociatedShpk; - public bool ApplyDyeTemplate(StmFile stm, int colorSetIdx, int rowIdx, StainId stainId) { if (colorSetIdx < 0 || colorSetIdx >= ColorDyeSets.Length || rowIdx is < 0 or >= ColorSet.RowArray.NumRows) @@ -79,7 +77,7 @@ public partial class MtrlFile : IWritable } - public List<(Sampler?, ShpkFile.Resource?)> GetSamplersByTexture() + public List<(Sampler?, ShpkFile.Resource?)> GetSamplersByTexture(ShpkFile? shpk) { var samplers = new List<(Sampler?, ShpkFile.Resource?)>(); for (var i = 0; i < Textures.Length; ++i) @@ -88,7 +86,7 @@ public partial class MtrlFile : IWritable } foreach (var sampler in ShaderPackage.Samplers) { - samplers[sampler.TextureIndex] = (sampler, AssociatedShpk?.GetSamplerById(sampler.SamplerId)); + samplers[sampler.TextureIndex] = (sampler, shpk?.GetSamplerById(sampler.SamplerId)); } return samplers; diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.ColorSet.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.ColorSet.cs new file mode 100644 index 00000000..d5cd5a44 --- /dev/null +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.ColorSet.cs @@ -0,0 +1,436 @@ +using System; +using System.Linq; +using System.Numerics; +using System.Runtime.InteropServices; +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.GameData.Files; +using Penumbra.String.Functions; + +namespace Penumbra.UI.Classes; + +public partial class ModEditWindow +{ + private static bool DrawMaterialColorSetChange( MtrlFile file, bool disabled ) + { + if( !file.ColorSets.Any( c => c.HasRows ) ) + { + return false; + } + + ColorSetCopyAllClipboardButton( file, 0 ); + ImGui.SameLine(); + var ret = ColorSetPasteAllClipboardButton( file, 0 ); + ImGui.SameLine(); + ImGui.Dummy( ImGuiHelpers.ScaledVector2( 20, 0 ) ); + ImGui.SameLine(); + ret |= DrawPreviewDye( file, disabled ); + + using var table = ImRaii.Table( "##ColorSets", 11, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV ); + if( !table ) + { + return false; + } + + ImGui.TableNextColumn(); + ImGui.TableHeader( string.Empty ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Row" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Diffuse" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Specular" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Emissive" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Gloss" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Tile" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Repeat" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Skew" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Dye" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Dye Preview" ); + + for( var j = 0; j < file.ColorSets.Length; ++j ) + { + using var _ = ImRaii.PushId( j ); + for( var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i ) + { + ret |= DrawColorSetRow( file, j, i, disabled ); + ImGui.TableNextRow(); + } + } + + return ret; + } + + + private static void ColorSetCopyAllClipboardButton( MtrlFile file, int colorSetIdx ) + { + if( !ImGui.Button( "Export All Rows to Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ) ) ) + { + return; + } + + try + { + var data1 = file.ColorSets[ colorSetIdx ].Rows.AsBytes(); + var data2 = file.ColorDyeSets.Length > colorSetIdx ? file.ColorDyeSets[ colorSetIdx ].Rows.AsBytes() : ReadOnlySpan< byte >.Empty; + var array = new byte[data1.Length + data2.Length]; + data1.TryCopyTo( array ); + data2.TryCopyTo( array.AsSpan( data1.Length ) ); + var text = Convert.ToBase64String( array ); + ImGui.SetClipboardText( text ); + } + catch + { + // ignored + } + } + + private static bool DrawPreviewDye( MtrlFile file, bool disabled ) + { + var (dyeId, (name, dyeColor, _)) = Penumbra.StainManager.StainCombo.CurrentSelection; + var tt = dyeId == 0 ? "Select a preview dye first." : "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled."; + if( ImGuiUtil.DrawDisabledButton( "Apply Preview Dye", Vector2.Zero, tt, disabled || dyeId == 0 ) ) + { + var ret = false; + for( var j = 0; j < file.ColorDyeSets.Length; ++j ) + { + for( var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i ) + { + ret |= file.ApplyDyeTemplate( Penumbra.StainManager.StmFile, j, i, dyeId ); + } + } + + return ret; + } + + ImGui.SameLine(); + var label = dyeId == 0 ? "Preview Dye###previewDye" : $"{name} (Preview)###previewDye"; + Penumbra.StainManager.StainCombo.Draw( label, dyeColor, string.Empty, true ); + return false; + } + + private static unsafe bool ColorSetPasteAllClipboardButton( MtrlFile file, int colorSetIdx ) + { + if( !ImGui.Button( "Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ) ) || file.ColorSets.Length <= colorSetIdx ) + { + return false; + } + + try + { + var text = ImGui.GetClipboardText(); + var data = Convert.FromBase64String( text ); + if( data.Length < Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() ) + { + return false; + } + + ref var rows = ref file.ColorSets[ colorSetIdx ].Rows; + fixed( void* ptr = data, output = &rows ) + { + MemoryUtility.MemCpyUnchecked( output, ptr, Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() ); + if( data.Length >= Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() + Marshal.SizeOf< MtrlFile.ColorDyeSet.RowArray >() + && file.ColorDyeSets.Length > colorSetIdx ) + { + ref var dyeRows = ref file.ColorDyeSets[ colorSetIdx ].Rows; + fixed( void* output2 = &dyeRows ) + { + MemoryUtility.MemCpyUnchecked( output2, ( byte* )ptr + Marshal.SizeOf< MtrlFile.ColorSet.RowArray >(), Marshal.SizeOf< MtrlFile.ColorDyeSet.RowArray >() ); + } + } + } + + return true; + } + catch + { + return false; + } + } + + private static unsafe void ColorSetCopyClipboardButton( MtrlFile.ColorSet.Row row, MtrlFile.ColorDyeSet.Row dye ) + { + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Clipboard.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + "Export this row to your clipboard.", false, true ) ) + { + try + { + var data = new byte[MtrlFile.ColorSet.Row.Size + 2]; + fixed( byte* ptr = data ) + { + MemoryUtility.MemCpyUnchecked( ptr, &row, MtrlFile.ColorSet.Row.Size ); + MemoryUtility.MemCpyUnchecked( ptr + MtrlFile.ColorSet.Row.Size, &dye, 2 ); + } + + var text = Convert.ToBase64String( data ); + ImGui.SetClipboardText( text ); + } + catch + { + // ignored + } + } + } + + private static unsafe bool ColorSetPasteFromClipboardButton( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled ) + { + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Paste.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + "Import an exported row from your clipboard onto this row.", disabled, true ) ) + { + try + { + var text = ImGui.GetClipboardText(); + var data = Convert.FromBase64String( text ); + if( data.Length != MtrlFile.ColorSet.Row.Size + 2 + || file.ColorSets.Length <= colorSetIdx ) + { + return false; + } + + fixed( byte* ptr = data ) + { + file.ColorSets[ colorSetIdx ].Rows[ rowIdx ] = *( MtrlFile.ColorSet.Row* )ptr; + if( colorSetIdx < file.ColorDyeSets.Length ) + { + file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] = *( MtrlFile.ColorDyeSet.Row* )( ptr + MtrlFile.ColorSet.Row.Size ); + } + } + + return true; + } + catch + { + // ignored + } + } + + return false; + } + + private static bool DrawColorSetRow( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled ) + { + static bool FixFloat( ref float val, float current ) + { + val = ( float )( Half )val; + return val != current; + } + + using var id = ImRaii.PushId( rowIdx ); + var row = file.ColorSets[ colorSetIdx ].Rows[ rowIdx ]; + var hasDye = file.ColorDyeSets.Length > colorSetIdx; + var dye = hasDye ? file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] : new MtrlFile.ColorDyeSet.Row(); + var floatSize = 70 * ImGuiHelpers.GlobalScale; + var intSize = 45 * ImGuiHelpers.GlobalScale; + ImGui.TableNextColumn(); + ColorSetCopyClipboardButton( row, dye ); + ImGui.SameLine(); + var ret = ColorSetPasteFromClipboardButton( file, colorSetIdx, rowIdx, disabled ); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted( $"#{rowIdx + 1:D2}" ); + + ImGui.TableNextColumn(); + using var dis = ImRaii.Disabled( disabled ); + ret |= ColorPicker( "##Diffuse", "Diffuse Color", row.Diffuse, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Diffuse = c ); + if( hasDye ) + { + ImGui.SameLine(); + ret |= ImGuiUtil.Checkbox( "##dyeDiffuse", "Apply Diffuse Color on Dye", dye.Diffuse, + b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Diffuse = b, ImGuiHoveredFlags.AllowWhenDisabled ); + } + + ImGui.TableNextColumn(); + ret |= ColorPicker( "##Specular", "Specular Color", row.Specular, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Specular = c ); + ImGui.SameLine(); + var tmpFloat = row.SpecularStrength; + ImGui.SetNextItemWidth( floatSize ); + if( ImGui.DragFloat( "##SpecularStrength", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.SpecularStrength ) ) + { + file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = tmpFloat; + ret = true; + } + + ImGuiUtil.HoverTooltip( "Specular Strength", ImGuiHoveredFlags.AllowWhenDisabled ); + + if( hasDye ) + { + ImGui.SameLine(); + ret |= ImGuiUtil.Checkbox( "##dyeSpecular", "Apply Specular Color on Dye", dye.Specular, + b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Specular = b, ImGuiHoveredFlags.AllowWhenDisabled ); + ImGui.SameLine(); + ret |= ImGuiUtil.Checkbox( "##dyeSpecularStrength", "Apply Specular Strength on Dye", dye.SpecularStrength, + b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = b, ImGuiHoveredFlags.AllowWhenDisabled ); + } + + ImGui.TableNextColumn(); + ret |= ColorPicker( "##Emissive", "Emissive Color", row.Emissive, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Emissive = c ); + if( hasDye ) + { + ImGui.SameLine(); + ret |= ImGuiUtil.Checkbox( "##dyeEmissive", "Apply Emissive Color on Dye", dye.Emissive, + b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Emissive = b, ImGuiHoveredFlags.AllowWhenDisabled ); + } + + ImGui.TableNextColumn(); + tmpFloat = row.GlossStrength; + ImGui.SetNextItemWidth( floatSize ); + if( ImGui.DragFloat( "##GlossStrength", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.GlossStrength ) ) + { + file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].GlossStrength = tmpFloat; + ret = true; + } + + ImGuiUtil.HoverTooltip( "Gloss Strength", ImGuiHoveredFlags.AllowWhenDisabled ); + if( hasDye ) + { + ImGui.SameLine(); + ret |= ImGuiUtil.Checkbox( "##dyeGloss", "Apply Gloss Strength on Dye", dye.Gloss, + b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Gloss = b, ImGuiHoveredFlags.AllowWhenDisabled ); + } + + ImGui.TableNextColumn(); + int tmpInt = row.TileSet; + ImGui.SetNextItemWidth( intSize ); + if( ImGui.InputInt( "##TileSet", ref tmpInt, 0, 0 ) && tmpInt != row.TileSet && tmpInt is >= 0 and <= ushort.MaxValue ) + { + file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].TileSet = ( ushort )tmpInt; + ret = true; + } + + ImGuiUtil.HoverTooltip( "Tile Set", ImGuiHoveredFlags.AllowWhenDisabled ); + + ImGui.TableNextColumn(); + tmpFloat = row.MaterialRepeat.X; + ImGui.SetNextItemWidth( floatSize ); + if( ImGui.DragFloat( "##RepeatX", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialRepeat.X ) ) + { + file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { X = tmpFloat }; + ret = true; + } + + ImGuiUtil.HoverTooltip( "Repeat X", ImGuiHoveredFlags.AllowWhenDisabled ); + ImGui.SameLine(); + tmpFloat = row.MaterialRepeat.Y; + ImGui.SetNextItemWidth( floatSize ); + if( ImGui.DragFloat( "##RepeatY", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialRepeat.Y ) ) + { + file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat }; + ret = true; + } + + ImGuiUtil.HoverTooltip( "Repeat Y", ImGuiHoveredFlags.AllowWhenDisabled ); + + ImGui.TableNextColumn(); + tmpFloat = row.MaterialSkew.X; + ImGui.SetNextItemWidth( floatSize ); + if( ImGui.DragFloat( "##SkewX", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialSkew.X ) ) + { + file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { X = tmpFloat }; + ret = true; + } + + ImGuiUtil.HoverTooltip( "Skew X", ImGuiHoveredFlags.AllowWhenDisabled ); + + ImGui.SameLine(); + tmpFloat = row.MaterialSkew.Y; + ImGui.SetNextItemWidth( floatSize ); + if( ImGui.DragFloat( "##SkewY", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialSkew.Y ) ) + { + file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { Y = tmpFloat }; + ret = true; + } + + ImGuiUtil.HoverTooltip( "Skew Y", ImGuiHoveredFlags.AllowWhenDisabled ); + + ImGui.TableNextColumn(); + if( hasDye ) + { + if( Penumbra.StainManager.TemplateCombo.Draw( "##dyeTemplate", dye.Template.ToString(), string.Empty, intSize + + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton ) ) + { + file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = Penumbra.StainManager.TemplateCombo.CurrentSelection; + ret = true; + } + + ImGuiUtil.HoverTooltip( "Dye Template", ImGuiHoveredFlags.AllowWhenDisabled ); + + ImGui.TableNextColumn(); + ret |= DrawDyePreview( file, colorSetIdx, rowIdx, disabled, dye, floatSize ); + } + else + { + ImGui.TableNextColumn(); + } + + + return ret; + } + + private static bool DrawDyePreview( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled, MtrlFile.ColorDyeSet.Row dye, float floatSize ) + { + var stain = Penumbra.StainManager.StainCombo.CurrentSelection.Key; + if( stain == 0 || !Penumbra.StainManager.StmFile.Entries.TryGetValue( dye.Template, out var entry ) ) + { + return false; + } + + var values = entry[ ( int )stain ]; + using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing / 2 ); + + var ret = ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2( ImGui.GetFrameHeight() ), + "Apply the selected dye to this row.", disabled, true ); + + ret = ret && file.ApplyDyeTemplate( Penumbra.StainManager.StmFile, colorSetIdx, rowIdx, stain ); + + ImGui.SameLine(); + ColorPicker( "##diffusePreview", string.Empty, values.Diffuse, _ => { }, "D" ); + ImGui.SameLine(); + ColorPicker( "##specularPreview", string.Empty, values.Specular, _ => { }, "S" ); + ImGui.SameLine(); + ColorPicker( "##emissivePreview", string.Empty, values.Emissive, _ => { }, "E" ); + ImGui.SameLine(); + using var dis = ImRaii.Disabled(); + ImGui.SetNextItemWidth( floatSize ); + ImGui.DragFloat( "##gloss", ref values.Gloss, 0, 0, 0, "%.2f G" ); + ImGui.SameLine(); + ImGui.SetNextItemWidth( floatSize ); + ImGui.DragFloat( "##specularStrength", ref values.SpecularPower, 0, 0, 0, "%.2f S" ); + + return ret; + } + + private static bool ColorPicker( string label, string tooltip, Vector3 input, Action< Vector3 > setter, string letter = "" ) + { + var ret = false; + var tmp = input; + if( ImGui.ColorEdit3( label, ref tmp, + ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.DisplayRGB | ImGuiColorEditFlags.InputRGB | ImGuiColorEditFlags.NoTooltip ) + && tmp != input ) + { + setter( tmp ); + ret = true; + } + + if( letter.Length > 0 && ImGui.IsItemVisible() ) + { + var textSize = ImGui.CalcTextSize( letter ); + var center = ImGui.GetItemRectMin() + ( ImGui.GetItemRectSize() - textSize ) / 2; + var textColor = input.LengthSquared() < 0.25f ? 0x80FFFFFFu : 0x80000000u; + ImGui.GetWindowDrawList().AddText( center, textColor, letter ); + } + + ImGuiUtil.HoverTooltip( tooltip, ImGuiHoveredFlags.AllowWhenDisabled ); + + return ret; + } +} \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs new file mode 100644 index 00000000..2280d316 --- /dev/null +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Dalamud.Interface; +using Dalamud.Interface.Internal.Notifications; +using ImGuiNET; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Raii; +using Penumbra.GameData.Data; +using Penumbra.GameData.Files; +using Penumbra.String.Classes; +using Penumbra.Util; + +namespace Penumbra.UI.Classes; + +public partial class ModEditWindow +{ + private sealed class MtrlTab : IWritable + { + private readonly ModEditWindow _edit; + public readonly MtrlFile Mtrl; + + public uint MaterialNewKeyId = 0; + public uint MaterialNewKeyDefault = 0; + public uint MaterialNewConstantId = 0; + public uint MaterialNewSamplerId = 0; + + + public ShpkFile? AssociatedShpk; + public readonly List< string > TextureLabels = new(4); + public FullPath LoadedShpkPath = FullPath.Empty; + public string LoadedShpkPathName = string.Empty; + public float TextureLabelWidth = 0f; + + // Shader Key State + public readonly List< string > ShaderKeyLabels = new(16); + public readonly Dictionary< uint, uint > DefinedShaderKeys = new(16); + public readonly List< int > MissingShaderKeyIndices = new(16); + public readonly List< uint > AvailableKeyValues = new(16); + public string VertexShaders = "Vertex Shaders: ???"; + public string PixelShaders = "Pixel Shaders: ???"; + + public FullPath FindAssociatedShpk( out string defaultPath, out Utf8GamePath defaultGamePath ) + { + defaultPath = GamePaths.Shader.ShpkPath( Mtrl.ShaderPackage.Name ); + if( !Utf8GamePath.FromString( defaultPath, out defaultGamePath, true ) ) + { + return FullPath.Empty; + } + + return _edit.FindBestMatch( defaultGamePath ); + } + + public void LoadShpk( FullPath path ) + { + try + { + LoadedShpkPath = path; + var data = LoadedShpkPath.IsRooted + ? File.ReadAllBytes( LoadedShpkPath.FullName ) + : Dalamud.GameData.GetFile( LoadedShpkPath.InternalName.ToString() )?.Data; + AssociatedShpk = data?.Length > 0 ? new ShpkFile( data ) : throw new Exception( "Failure to load file data." ); + LoadedShpkPathName = path.ToPath(); + } + catch( Exception e ) + { + LoadedShpkPath = FullPath.Empty; + LoadedShpkPathName = string.Empty; + AssociatedShpk = null; + ChatUtil.NotificationMessage( $"Could not load {LoadedShpkPath.ToPath()}:\n{e}", "Penumbra Advanced Editing", NotificationType.Error ); + } + + Update(); + } + + public void UpdateTextureLabels() + { + var samplers = Mtrl.GetSamplersByTexture( AssociatedShpk ); + TextureLabels.Clear(); + TextureLabelWidth = 50f * ImGuiHelpers.GlobalScale; + using( var _ = ImRaii.PushFont( UiBuilder.MonoFont ) ) + { + for( var i = 0; i < Mtrl.Textures.Length; ++i ) + { + var (sampler, shpkSampler) = samplers[ i ]; + var name = shpkSampler.HasValue ? shpkSampler.Value.Name : sampler.HasValue ? $"0x{sampler.Value.SamplerId:X8}" : $"#{i}"; + TextureLabels.Add( name ); + TextureLabelWidth = Math.Max( TextureLabelWidth, ImGui.CalcTextSize( name ).X ); + } + } + + TextureLabelWidth = TextureLabelWidth / ImGuiHelpers.GlobalScale + 4; + } + + public void UpdateShaderKeyLabels() + { + ShaderKeyLabels.Clear(); + DefinedShaderKeys.Clear(); + foreach( var (key, idx) in Mtrl.ShaderPackage.ShaderKeys.WithIndex() ) + { + ShaderKeyLabels.Add( $"#{idx}: 0x{key.Category:X8} = 0x{key.Value:X8}###{idx}: 0x{key.Category:X8}" ); + DefinedShaderKeys.Add( key.Category, key.Value ); + } + + MissingShaderKeyIndices.Clear(); + AvailableKeyValues.Clear(); + var vertexShaders = new IndexSet( AssociatedShpk?.VertexShaders.Length ?? 0, false ); + var pixelShaders = new IndexSet( AssociatedShpk?.PixelShaders.Length ?? 0, false ); + if( AssociatedShpk != null ) + { + MissingShaderKeyIndices.AddRange( AssociatedShpk.MaterialKeys.WithIndex().Where( k => !DefinedShaderKeys.ContainsKey( k.Value.Id ) ).WithoutValue() ); + + if( MissingShaderKeyIndices.Count > 0 && MissingShaderKeyIndices.All( i => AssociatedShpk.MaterialKeys[ i ].Id != MaterialNewKeyId ) ) + { + var key = AssociatedShpk.MaterialKeys[ MissingShaderKeyIndices[ 0 ] ]; + MaterialNewKeyId = key.Id; + MaterialNewKeyDefault = key.DefaultValue; + } + + AvailableKeyValues.AddRange( AssociatedShpk.MaterialKeys.Select( k => DefinedShaderKeys.TryGetValue( k.Id, out var value ) ? value : k.DefaultValue ) ); + foreach( var node in AssociatedShpk.Nodes ) + { + if( node.MaterialKeys.WithIndex().All( key => key.Value == AvailableKeyValues[ key.Index ] ) ) + { + foreach( var pass in node.Passes ) + { + vertexShaders.Add( ( int )pass.VertexShader ); + pixelShaders.Add( ( int )pass.PixelShader ); + } + } + } + } + + VertexShaders = $"Vertex Shaders: {( vertexShaders.Count > 0 ? string.Join( ", ", vertexShaders.Select( i => $"#{i}" ) ) : "???" )}"; + PixelShaders = $"Pixel Shaders: {( pixelShaders.Count > 0 ? string.Join( ", ", pixelShaders.Select( i => $"#{i}" ) ) : "???" )}"; + } + + public void Update() + { + UpdateTextureLabels(); + UpdateShaderKeyLabels(); + } + + public MtrlTab( ModEditWindow edit, MtrlFile file ) + { + _edit = edit; + Mtrl = file; + LoadShpk( FindAssociatedShpk( out _, out _ ) ); + } + + public bool Valid + => Mtrl.Valid; + + public byte[] Write() + => Mtrl.Write(); + } +} \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs index 67d9996d..8f3dfb1e 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs @@ -5,17 +5,14 @@ using System.Linq; using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; -using Dalamud.Interface.Internal.Notifications; using ImGuiNET; using Lumina.Data.Parsing; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using Penumbra.GameData; -using Penumbra.GameData.Data; using Penumbra.GameData.Files; using Penumbra.String.Classes; -using Penumbra.Util; namespace Penumbra.UI.Classes; @@ -23,73 +20,20 @@ public partial class ModEditWindow { private readonly FileDialogManager _materialFileDialog = ConfigWindow.SetupFileManager(); - private FullPath FindAssociatedShpk( MtrlFile mtrl, out string defaultPath, out Utf8GamePath defaultGamePath ) - { - defaultPath = GamePaths.Shader.ShpkPath( mtrl.ShaderPackage.Name ); - if( !Utf8GamePath.FromString( defaultPath, out defaultGamePath, true ) ) - { - return FullPath.Empty; - } - - return FindBestMatch( defaultGamePath ); - } - - private void LoadAssociatedShpk( MtrlFile mtrl ) - { - try - { - _mtrlTabState.LoadedShpkPath = FindAssociatedShpk( mtrl, out _, out _ ); - var data = _mtrlTabState.LoadedShpkPath.IsRooted - ? File.ReadAllBytes( _mtrlTabState.LoadedShpkPath.FullName ) - : Dalamud.GameData.GetFile( _mtrlTabState.LoadedShpkPath.InternalName.ToString() )?.Data; - if( data?.Length > 0 ) - { - mtrl.AssociatedShpk = new ShpkFile( data ); - } - } - catch( Exception e ) - { - Penumbra.Log.Debug( $"Could not parse associated file {_mtrlTabState.LoadedShpkPath} to Shpk:\n{e}" ); - _mtrlTabState.LoadedShpkPath = FullPath.Empty; - mtrl.AssociatedShpk = null; - } - - UpdateTextureLabels( mtrl ); - } - - private void UpdateTextureLabels( MtrlFile file ) - { - var samplers = file.GetSamplersByTexture(); - _mtrlTabState.TextureLabels.Clear(); - _mtrlTabState.TextureLabelWidth = 50f * ImGuiHelpers.GlobalScale; - using( var _ = ImRaii.PushFont( UiBuilder.MonoFont ) ) - { - for( var i = 0; i < file.Textures.Length; ++i ) - { - var (sampler, shpkSampler) = samplers[ i ]; - var name = shpkSampler.HasValue ? shpkSampler.Value.Name : sampler.HasValue ? $"0x{sampler.Value.SamplerId:X8}" : $"#{i}"; - _mtrlTabState.TextureLabels.Add( name ); - _mtrlTabState.TextureLabelWidth = Math.Max( _mtrlTabState.TextureLabelWidth, ImGui.CalcTextSize( name ).X ); - } - } - - _mtrlTabState.TextureLabelWidth = _mtrlTabState.TextureLabelWidth / ImGuiHelpers.GlobalScale + 4; - } - - private bool DrawPackageNameInput( MtrlFile file, bool disabled ) + private bool DrawPackageNameInput( MtrlTab tab, bool disabled ) { var ret = false; ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - if( ImGui.InputText( "Shader Package Name", ref file.ShaderPackage.Name, 63, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) + if( ImGui.InputText( "Shader Package Name", ref tab.Mtrl.ShaderPackage.Name, 63, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) { - ret = true; - file.AssociatedShpk = null; - _mtrlTabState.LoadedShpkPath = FullPath.Empty; + ret = true; + tab.AssociatedShpk = null; + tab.LoadedShpkPath = FullPath.Empty; } if( ImGui.IsItemDeactivatedAfterEdit() ) { - LoadAssociatedShpk( file ); + tab.LoadShpk( tab.FindAssociatedShpk( out _, out _ ) ); } return ret; @@ -114,20 +58,20 @@ public partial class ModEditWindow /// Show the currently associated shpk file, if any, and the buttons to associate /// a specific shpk from your drive, the modded shpk by path or the default shpk. /// - private void DrawCustomAssociations( MtrlFile file ) + private void DrawCustomAssociations( MtrlTab tab ) { - var text = file.AssociatedShpk == null + var text = tab.AssociatedShpk == null ? "Associated .shpk file: None" - : $"Associated .shpk file: {_mtrlTabState.LoadedShpkPath.ToPath()}"; + : $"Associated .shpk file: {tab.LoadedShpkPathName}"; ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); if( ImGui.Selectable( text ) ) { - ImGui.SetClipboardText( _mtrlTabState.LoadedShpkPath.IsRooted ? _mtrlTabState.LoadedShpkPath.FullName.Replace( "\\", "/" ) ); + ImGui.SetClipboardText( tab.LoadedShpkPathName ); } - ImGuiUtil.HoverTooltip( "Click to copy file path to clipboard." ); + ImGuiUtil.HoverTooltip( "Click to copy file path to clipboard." ); if( ImGui.Button( "Associate Custom .shpk File" ) ) { @@ -138,358 +82,323 @@ public partial class ModEditWindow return; } - try - { - file.AssociatedShpk = new ShpkFile( File.ReadAllBytes( name ) ); - _mtrlTabState.LoadedShpkPath = new FullPath( name ); - } - catch( Exception e ) - { - ChatUtil.NotificationMessage( $"Could not load {name}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); - } - }); + tab.LoadShpk( new FullPath( name ) ); + } ); } - var moddedPath = FindAssociatedShpk( file, out var defaultPath, out var gamePath ); + var moddedPath = tab.FindAssociatedShpk( out var defaultPath, out var gamePath ); ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( "Associate Default .shpk File", Vector2.Zero, moddedPath.ToPath(), moddedPath.Equals( _mtrlTabState.LoadedShpkPath ) ) ) + if( ImGuiUtil.DrawDisabledButton( "Associate Default .shpk File", Vector2.Zero, moddedPath.ToPath(), moddedPath.Equals( tab.LoadedShpkPath ) ) ) { - LoadAssociatedShpk( file ); - if( file.AssociatedShpk == null ) - { - ChatUtil.NotificationMessage( $"Could not load {moddedPath}.", "Penumbra Advanced Editing", NotificationType.Error ); - } + tab.LoadShpk( moddedPath ); } if( !gamePath.Path.Equals( moddedPath.InternalName ) ) { ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( "Associate Unmodded .shpk File", Vector2.Zero, defaultPath, gamePath.Path.Equals( _mtrlTabState.LoadedShpkPath.InternalName ) ) ) + if( ImGuiUtil.DrawDisabledButton( "Associate Unmodded .shpk File", Vector2.Zero, defaultPath, gamePath.Path.Equals( tab.LoadedShpkPath.InternalName ) ) ) { - try - { - file.AssociatedShpk = new ShpkFile( Dalamud.GameData.GetFile( defaultPath )?.Data ?? Array.Empty< byte >() ); - _mtrlTabState.LoadedShpkPath = new FullPath( defaultPath ); - } - catch( Exception e ) - { - ChatUtil.NotificationMessage( $"Could not load {defaultPath}:\n{e}", "Penumbra Advanced Editing", NotificationType.Error ); - } + tab.LoadShpk( new FullPath( gamePath ) ); } } ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); } - private bool DrawMaterialShaderKeys( MtrlFile file, bool disabled ) + + private static bool DrawShaderKey( MtrlTab tab, bool disabled, ShaderKey key, int idx ) { - var ret = false; - if( file.ShaderPackage.ShaderKeys.Length > 0 || !disabled && file.AssociatedShpk != null && file.AssociatedShpk.MaterialKeys.Length > 0 ) + var ret = false; + using var t2 = ImRaii.TreeNode( tab.ShaderKeyLabels[ idx ], disabled ? ImGuiTreeNodeFlags.Leaf : 0 ); + if( !t2 || disabled ) { - using var t = ImRaii.TreeNode( "Shader Keys" ); - if( t ) + return ret; + } + + var shpkKey = tab.AssociatedShpk?.GetMaterialKeyById( key.Category ); + if( shpkKey.HasValue ) + { + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + using var c = ImRaii.Combo( "Value", $"0x{key.Value:X8}" ); + if( c ) { - var definedKeys = new HashSet(); - - foreach( var (key, idx) in file.ShaderPackage.ShaderKeys.WithIndex() ) + foreach( var value in shpkKey.Value.Values ) { - definedKeys.Add( key.Category ); - using var t2 = ImRaii.TreeNode( $"#{idx}: 0x{key.Category:X8} = 0x{key.Value:X8}###{idx}: 0x{key.Category:X8}", disabled ? ImGuiTreeNodeFlags.Leaf : 0 ); - if( t2 ) + if( ImGui.Selectable( $"0x{value:X8}", value == key.Value ) ) { - if( !disabled ) - { - var shpkKey = file.AssociatedShpk?.GetMaterialKeyById( key.Category ); - if( shpkKey.HasValue ) - { - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - using var c = ImRaii.Combo( "Value", $"0x{key.Value:X8}" ); - if( c ) - { - foreach( var value in shpkKey.Value.Values ) - { - if( ImGui.Selectable( $"0x{value:X8}", value == key.Value ) ) - { - file.ShaderPackage.ShaderKeys[idx].Value = value; - ret = true; - } - } - } - } - - if( ImGui.Button( "Remove Key" ) ) - { - file.ShaderPackage.ShaderKeys = file.ShaderPackage.ShaderKeys.RemoveItems( idx ); - ret = true; - } - } - } - } - - if( !disabled && file.AssociatedShpk != null ) - { - var missingKeys = file.AssociatedShpk.MaterialKeys.Where( key => !definedKeys.Contains( key.Id ) ).ToArray(); - if( missingKeys.Length > 0 ) - { - var selectedKey = Array.Find( missingKeys, key => key.Id == _mtrlTabState.MaterialNewKeyId ); - if( Array.IndexOf( missingKeys, selectedKey ) < 0 ) - { - selectedKey = missingKeys[0]; - _mtrlTabState.MaterialNewKeyId = selectedKey.Id; - } - - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - using( var c = ImRaii.Combo( "##NewConstantId", $"ID: 0x{selectedKey.Id:X8}" ) ) - { - if( c ) - { - foreach( var key in missingKeys ) - { - if( ImGui.Selectable( $"ID: 0x{key.Id:X8}", key.Id == _mtrlTabState.MaterialNewKeyId ) ) - { - selectedKey = key; - _mtrlTabState.MaterialNewKeyId = key.Id; - } - } - } - } - - ImGui.SameLine(); - if( ImGui.Button( "Add Key" ) ) - { - file.ShaderPackage.ShaderKeys = file.ShaderPackage.ShaderKeys.AddItem( new ShaderKey - { - Category = selectedKey.Id, - Value = selectedKey.DefaultValue, - } ); - ret = true; - } + tab.Mtrl.ShaderPackage.ShaderKeys[ idx ].Value = value; + ret = true; } } } } + if( ImGui.Button( "Remove Key" ) ) + { + tab.Mtrl.ShaderPackage.ShaderKeys = tab.Mtrl.ShaderPackage.ShaderKeys.RemoveItems( idx ); + ret = true; + } + return ret; } - private void DrawMaterialShaders( MtrlFile file ) + private static bool DrawNewShaderKey( MtrlTab tab ) { - if( file.AssociatedShpk != null ) + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + using( var c = ImRaii.Combo( "##NewConstantId", $"ID: 0x{tab.MaterialNewKeyId:X8}" ) ) { - var definedKeys = new Dictionary(); - foreach( var key in file.ShaderPackage.ShaderKeys ) + if( c ) { - definedKeys[key.Category] = key.Value; - } - - var materialKeys = Array.ConvertAll( file.AssociatedShpk.MaterialKeys, key => - { - if( definedKeys.TryGetValue( key.Id, out var value ) ) + foreach( var idx in tab.MissingShaderKeyIndices ) { - return value; + var key = tab.AssociatedShpk!.MaterialKeys[ idx ]; + + if( ImGui.Selectable( $"ID: 0x{key.Id:X8}", key.Id == tab.MaterialNewKeyId ) ) + { + tab.MaterialNewKeyDefault = key.DefaultValue; + tab.MaterialNewKeyId = key.Id; + } + } + } + } + + ImGui.SameLine(); + if( ImGui.Button( "Add Key" ) ) + { + tab.Mtrl.ShaderPackage.ShaderKeys = tab.Mtrl.ShaderPackage.ShaderKeys.AddItem( new ShaderKey + { + Category = tab.MaterialNewKeyId, + Value = tab.MaterialNewKeyDefault, + } ); + return true; + } + + return false; + } + + private static bool DrawMaterialShaderKeys( MtrlTab tab, bool disabled ) + { + if( tab.Mtrl.ShaderPackage.ShaderKeys.Length <= 0 && ( disabled || tab.AssociatedShpk == null || tab.AssociatedShpk.MaterialKeys.Length <= 0 ) ) + { + return false; + } + + using var t = ImRaii.TreeNode( "Shader Keys" ); + if( !t ) + { + return false; + } + + var ret = false; + foreach( var (key, idx) in tab.Mtrl.ShaderPackage.ShaderKeys.WithIndex() ) + { + ret |= DrawShaderKey( tab, disabled, key, idx ); + } + + if( !disabled && tab.AssociatedShpk != null && tab.MissingShaderKeyIndices.Count != 0 ) + { + ret |= DrawNewShaderKey( tab ); + } + + if( ret ) + { + tab.UpdateShaderKeyLabels(); + } + + return ret; + } + + + private static void DrawMaterialShaders( MtrlTab tab ) + { + if( tab.AssociatedShpk == null ) + { + return; + } + + ImRaii.TreeNode( tab.VertexShaders, ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode( tab.PixelShaders, ImGuiTreeNodeFlags.Leaf ).Dispose(); + } + + private bool DrawMaterialConstants( MtrlTab tab, bool disabled ) + { + var ret = false; + if( tab.Mtrl.ShaderPackage.Constants.Length <= 0 + && tab.Mtrl.ShaderPackage.ShaderValues.Length <= 0 + && ( disabled || tab.AssociatedShpk == null || tab.AssociatedShpk.Constants.Length <= 0 ) ) + { + return ret; + } + + var materialParams = tab.AssociatedShpk?.GetConstantById( ShpkFile.MaterialParamsConstantId ); + + using var t = ImRaii.TreeNode( materialParams?.Name ?? "Constants" ); + if( t ) + { + var orphanValues = new IndexSet( tab.Mtrl.ShaderPackage.ShaderValues.Length, true ); + var aliasedValueCount = 0; + var definedConstants = new HashSet< uint >(); + var hasMalformedConstants = false; + + foreach( var constant in tab.Mtrl.ShaderPackage.Constants ) + { + definedConstants.Add( constant.Id ); + var values = tab.Mtrl.GetConstantValues( constant ); + if( tab.Mtrl.GetConstantValues( constant ).Length > 0 ) + { + var unique = orphanValues.RemoveRange( constant.ByteOffset >> 2, values.Length ); + aliasedValueCount += values.Length - unique; } else { - return key.DefaultValue; - } - } ); - var vertexShaders = new IndexSet( file.AssociatedShpk.VertexShaders.Length, false ); - var pixelShaders = new IndexSet( file.AssociatedShpk.PixelShaders.Length, false ); - foreach( var node in file.AssociatedShpk.Nodes ) - { - if( node.MaterialKeys.WithIndex().All( key => key.Value == materialKeys[key.Index] ) ) - { - foreach( var pass in node.Passes ) - { - vertexShaders.Add( ( int )pass.VertexShader ); - pixelShaders.Add( ( int )pass.PixelShader ); - } + hasMalformedConstants = true; } } - ImRaii.TreeNode( $"Vertex Shaders: {( vertexShaders.Count > 0 ? string.Join( ", ", vertexShaders.Select( i => $"#{i}" ) ) : "???" )}", ImGuiTreeNodeFlags.Leaf ) - .Dispose(); - ImRaii.TreeNode( $"Pixel Shaders: {( pixelShaders.Count > 0 ? string.Join( ", ", pixelShaders.Select( i => $"#{i}" ) ) : "???" )}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - } - - private bool DrawMaterialConstants( MtrlFile file, bool disabled ) - { - var ret = false; - if( file.ShaderPackage.Constants.Length > 0 - || file.ShaderPackage.ShaderValues.Length > 0 - || !disabled && file.AssociatedShpk != null && file.AssociatedShpk.Constants.Length > 0 ) - { - var materialParams = file.AssociatedShpk?.GetConstantById( ShpkFile.MaterialParamsConstantId ); - - using var t = ImRaii.TreeNode( materialParams?.Name ?? "Constants" ); - if( t ) + foreach( var (constant, idx) in tab.Mtrl.ShaderPackage.Constants.WithIndex() ) { - var orphanValues = new IndexSet( file.ShaderPackage.ShaderValues.Length, true ); - var aliasedValueCount = 0; - var definedConstants = new HashSet(); - var hasMalformedConstants = false; - - foreach( var constant in file.ShaderPackage.Constants ) + var values = tab.Mtrl.GetConstantValues( constant ); + var paramValueOffset = -values.Length; + if( values.Length > 0 ) { - definedConstants.Add( constant.Id ); - var values = file.GetConstantValues( constant ); - if( file.GetConstantValues( constant ).Length > 0 ) + var shpkParam = tab.AssociatedShpk?.GetMaterialParamById( constant.Id ); + var paramByteOffset = shpkParam.HasValue ? shpkParam.Value.ByteOffset : -1; + if( ( paramByteOffset & 0x3 ) == 0 ) { - var unique = orphanValues.RemoveRange( constant.ByteOffset >> 2, values.Length ); - aliasedValueCount += values.Length - unique; - } - else - { - hasMalformedConstants = true; + paramValueOffset = paramByteOffset >> 2; } } - foreach( var (constant, idx) in file.ShaderPackage.Constants.WithIndex() ) + var (constantName, componentOnly) = MaterialParamRangeName( materialParams?.Name ?? "", paramValueOffset, values.Length ); + + using var t2 = ImRaii.TreeNode( $"#{idx}{( constantName != null ? ": " + constantName : "" )} (ID: 0x{constant.Id:X8})" ); + if( t2 ) { - var values = file.GetConstantValues( constant ); - var paramValueOffset = -values.Length; if( values.Length > 0 ) { - var shpkParam = file.AssociatedShpk?.GetMaterialParamById( constant.Id ); - var paramByteOffset = shpkParam.HasValue ? shpkParam.Value.ByteOffset : -1; - if( ( paramByteOffset & 0x3 ) == 0 ) + var valueOffset = constant.ByteOffset >> 2; + + for( var valueIdx = 0; valueIdx < values.Length; ++valueIdx ) { - paramValueOffset = paramByteOffset >> 2; - } - } - - var (constantName, componentOnly) = MaterialParamRangeName( materialParams?.Name ?? "", paramValueOffset, values.Length ); - - using var t2 = ImRaii.TreeNode( $"#{idx}{( constantName != null ? ": " + constantName : "" )} (ID: 0x{constant.Id:X8})" ); - if( t2 ) - { - if( values.Length > 0 ) - { - var valueOffset = constant.ByteOffset >> 2; - - for( var valueIdx = 0; valueIdx < values.Length; ++valueIdx ) - { - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - if( ImGui.InputFloat( - $"{MaterialParamName( componentOnly, paramValueOffset + valueIdx ) ?? $"#{valueIdx}"} (at 0x{( valueOffset + valueIdx ) << 2:X4})", - ref values[valueIdx], 0.0f, 0.0f, "%.3f", - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) - { - ret = true; - } - } - } - else - { - ImRaii.TreeNode( $"Offset: 0x{constant.ByteOffset:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - ImRaii.TreeNode( $"Size: 0x{constant.ByteSize:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - - if( !disabled - && !hasMalformedConstants - && orphanValues.Count == 0 - && aliasedValueCount == 0 - && ImGui.Button( "Remove Constant" ) ) - { - file.ShaderPackage.ShaderValues = file.ShaderPackage.ShaderValues.RemoveItems( constant.ByteOffset >> 2, constant.ByteSize >> 2 ); - file.ShaderPackage.Constants = file.ShaderPackage.Constants.RemoveItems( idx ); - for( var i = 0; i < file.ShaderPackage.Constants.Length; ++i ) - { - if( file.ShaderPackage.Constants[i].ByteOffset >= constant.ByteOffset ) - { - file.ShaderPackage.Constants[i].ByteOffset -= constant.ByteSize; - } - } - - ret = true; - } - } - } - - if( orphanValues.Count > 0 ) - { - using var t2 = ImRaii.TreeNode( $"Orphan Values ({orphanValues.Count})" ); - if( t2 ) - { - foreach( var idx in orphanValues ) - { - ImGui.SetNextItemWidth( ImGui.GetFontSize() * 10.0f ); - if( ImGui.InputFloat( $"#{idx} (at 0x{idx << 2:X4})", - ref file.ShaderPackage.ShaderValues[idx], 0.0f, 0.0f, "%.3f", + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + if( ImGui.InputFloat( + $"{MaterialParamName( componentOnly, paramValueOffset + valueIdx ) ?? $"#{valueIdx}"} (at 0x{( valueOffset + valueIdx ) << 2:X4})", + ref values[ valueIdx ], 0.0f, 0.0f, "%.3f", disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) { ret = true; } } } - } - else if( !disabled && !hasMalformedConstants && file.AssociatedShpk != null ) - { - var missingConstants = file.AssociatedShpk.MaterialParams.Where( constant - => ( constant.ByteOffset & 0x3 ) == 0 && ( constant.ByteSize & 0x3 ) == 0 && !definedConstants.Contains( constant.Id ) ).ToArray(); - if( missingConstants.Length > 0 ) + else { - var selectedConstant = Array.Find( missingConstants, constant => constant.Id == _mtrlTabState.MaterialNewConstantId ); - if( selectedConstant.ByteSize == 0 ) - { - selectedConstant = missingConstants[0]; - _mtrlTabState.MaterialNewConstantId = selectedConstant.Id; - } + ImRaii.TreeNode( $"Offset: 0x{constant.ByteOffset:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode( $"Size: 0x{constant.ByteSize:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + } - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 450.0f ); - var (selectedConstantName, _) = MaterialParamRangeName( materialParams?.Name ?? "", selectedConstant.ByteOffset >> 2, selectedConstant.ByteSize >> 2 ); - using( var c = ImRaii.Combo( "##NewConstantId", $"{selectedConstantName} (ID: 0x{selectedConstant.Id:X8})" ) ) + if( !disabled + && !hasMalformedConstants + && orphanValues.Count == 0 + && aliasedValueCount == 0 + && ImGui.Button( "Remove Constant" ) ) + { + tab.Mtrl.ShaderPackage.ShaderValues = tab.Mtrl.ShaderPackage.ShaderValues.RemoveItems( constant.ByteOffset >> 2, constant.ByteSize >> 2 ); + tab.Mtrl.ShaderPackage.Constants = tab.Mtrl.ShaderPackage.Constants.RemoveItems( idx ); + for( var i = 0; i < tab.Mtrl.ShaderPackage.Constants.Length; ++i ) { - if( c ) + if( tab.Mtrl.ShaderPackage.Constants[ i ].ByteOffset >= constant.ByteOffset ) { - foreach( var constant in missingConstants ) - { - var (constantName, _) = MaterialParamRangeName( materialParams?.Name ?? "", constant.ByteOffset >> 2, constant.ByteSize >> 2 ); - if( ImGui.Selectable( $"{constantName} (ID: 0x{constant.Id:X8})", constant.Id == _mtrlTabState.MaterialNewConstantId ) ) - { - selectedConstant = constant; - _mtrlTabState.MaterialNewConstantId = constant.Id; - } - } + tab.Mtrl.ShaderPackage.Constants[ i ].ByteOffset -= constant.ByteSize; } } - ImGui.SameLine(); - if( ImGui.Button( "Add Constant" ) ) + ret = true; + } + } + } + + if( orphanValues.Count > 0 ) + { + using var t2 = ImRaii.TreeNode( $"Orphan Values ({orphanValues.Count})" ); + if( t2 ) + { + foreach( var idx in orphanValues ) + { + ImGui.SetNextItemWidth( ImGui.GetFontSize() * 10.0f ); + if( ImGui.InputFloat( $"#{idx} (at 0x{idx << 2:X4})", + ref tab.Mtrl.ShaderPackage.ShaderValues[ idx ], 0.0f, 0.0f, "%.3f", + disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) { - file.ShaderPackage.ShaderValues = file.ShaderPackage.ShaderValues.AddItem( 0.0f, selectedConstant.ByteSize >> 2 ); - file.ShaderPackage.Constants = file.ShaderPackage.Constants.AddItem( new MtrlFile.Constant - { - Id = _mtrlTabState.MaterialNewConstantId, - ByteOffset = ( ushort )( file.ShaderPackage.ShaderValues.Length << 2 ), - ByteSize = selectedConstant.ByteSize, - } ); ret = true; } } } } + else if( !disabled && !hasMalformedConstants && tab.AssociatedShpk != null ) + { + var missingConstants = tab.AssociatedShpk.MaterialParams.Where( constant + => ( constant.ByteOffset & 0x3 ) == 0 && ( constant.ByteSize & 0x3 ) == 0 && !definedConstants.Contains( constant.Id ) ).ToArray(); + if( missingConstants.Length > 0 ) + { + var selectedConstant = Array.Find( missingConstants, constant => constant.Id == tab.MaterialNewConstantId ); + if( selectedConstant.ByteSize == 0 ) + { + selectedConstant = missingConstants[ 0 ]; + tab.MaterialNewConstantId = selectedConstant.Id; + } + + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 450.0f ); + var (selectedConstantName, _) = MaterialParamRangeName( materialParams?.Name ?? "", selectedConstant.ByteOffset >> 2, selectedConstant.ByteSize >> 2 ); + using( var c = ImRaii.Combo( "##NewConstantId", $"{selectedConstantName} (ID: 0x{selectedConstant.Id:X8})" ) ) + { + if( c ) + { + foreach( var constant in missingConstants ) + { + var (constantName, _) = MaterialParamRangeName( materialParams?.Name ?? "", constant.ByteOffset >> 2, constant.ByteSize >> 2 ); + if( ImGui.Selectable( $"{constantName} (ID: 0x{constant.Id:X8})", constant.Id == tab.MaterialNewConstantId ) ) + { + selectedConstant = constant; + tab.MaterialNewConstantId = constant.Id; + } + } + } + } + + ImGui.SameLine(); + if( ImGui.Button( "Add Constant" ) ) + { + tab.Mtrl.ShaderPackage.ShaderValues = tab.Mtrl.ShaderPackage.ShaderValues.AddItem( 0.0f, selectedConstant.ByteSize >> 2 ); + tab.Mtrl.ShaderPackage.Constants = tab.Mtrl.ShaderPackage.Constants.AddItem( new MtrlFile.Constant + { + Id = tab.MaterialNewConstantId, + ByteOffset = ( ushort )( tab.Mtrl.ShaderPackage.ShaderValues.Length << 2 ), + ByteSize = selectedConstant.ByteSize, + } ); + ret = true; + } + } + } } return ret; } - private bool DrawMaterialSamplers( MtrlFile file, bool disabled ) + private bool DrawMaterialSamplers( MtrlTab tab, bool disabled ) { var ret = false; - if( file.ShaderPackage.Samplers.Length > 0 - || file.Textures.Length > 0 - || !disabled && file.AssociatedShpk != null && file.AssociatedShpk.Samplers.Any( sampler => sampler.Slot == 2 ) ) + if( tab.Mtrl.ShaderPackage.Samplers.Length > 0 + || tab.Mtrl.Textures.Length > 0 + || !disabled && tab.AssociatedShpk != null && tab.AssociatedShpk.Samplers.Any( sampler => sampler.Slot == 2 ) ) { using var t = ImRaii.TreeNode( "Samplers" ); if( t ) { - var orphanTextures = new IndexSet( file.Textures.Length, true ); + var orphanTextures = new IndexSet( tab.Mtrl.Textures.Length, true ); var aliasedTextureCount = 0; - var definedSamplers = new HashSet(); + var definedSamplers = new HashSet< uint >(); - foreach( var sampler in file.ShaderPackage.Samplers ) + foreach( var sampler in tab.Mtrl.ShaderPackage.Samplers ) { if( !orphanTextures.Remove( sampler.TextureIndex ) ) { @@ -499,13 +408,13 @@ public partial class ModEditWindow definedSamplers.Add( sampler.SamplerId ); } - foreach( var (sampler, idx) in file.ShaderPackage.Samplers.WithIndex() ) + foreach( var (sampler, idx) in tab.Mtrl.ShaderPackage.Samplers.WithIndex() ) { - var shpkSampler = file.AssociatedShpk?.GetSamplerById( sampler.SamplerId ); - using var t2 = ImRaii.TreeNode( $"#{idx}{( shpkSampler.HasValue ? ": " + shpkSampler.Value.Name : "" )} (ID: 0x{sampler.SamplerId:X8})" ); + var shpkSampler = tab.AssociatedShpk?.GetSamplerById( sampler.SamplerId ); + using var t2 = ImRaii.TreeNode( $"#{idx}{( shpkSampler.HasValue ? ": " + shpkSampler.Value.Name : "" )} (ID: 0x{sampler.SamplerId:X8})" ); if( t2 ) { - ImRaii.TreeNode( $"Texture: #{sampler.TextureIndex} - {Path.GetFileName( file.Textures[sampler.TextureIndex].Path )}", ImGuiTreeNodeFlags.Leaf ) + ImRaii.TreeNode( $"Texture: #{sampler.TextureIndex} - {Path.GetFileName( tab.Mtrl.Textures[ sampler.TextureIndex ].Path )}", ImGuiTreeNodeFlags.Leaf ) .Dispose(); // FIXME this probably doesn't belong here @@ -518,7 +427,8 @@ public partial class ModEditWindow } ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - if( InputHexUInt16( "Texture Flags", ref file.Textures[sampler.TextureIndex].Flags, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) + if( InputHexUInt16( "Texture Flags", ref tab.Mtrl.Textures[ sampler.TextureIndex ].Flags, + disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) { ret = true; } @@ -528,22 +438,22 @@ public partial class ModEditWindow if( ImGui.InputInt( "Sampler Flags", ref sampFlags, 0, 0, ImGuiInputTextFlags.CharsHexadecimal | ( disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) ) { - file.ShaderPackage.Samplers[idx].Flags = ( uint )sampFlags; - ret = true; + tab.Mtrl.ShaderPackage.Samplers[ idx ].Flags = ( uint )sampFlags; + ret = true; } if( !disabled && orphanTextures.Count == 0 - && aliasedTextureCount == 0 + && aliasedTextureCount == 0 && ImGui.Button( "Remove Sampler" ) ) { - file.Textures = file.Textures.RemoveItems( sampler.TextureIndex ); - file.ShaderPackage.Samplers = file.ShaderPackage.Samplers.RemoveItems( idx ); - for( var i = 0; i < file.ShaderPackage.Samplers.Length; ++i ) + tab.Mtrl.Textures = tab.Mtrl.Textures.RemoveItems( sampler.TextureIndex ); + tab.Mtrl.ShaderPackage.Samplers = tab.Mtrl.ShaderPackage.Samplers.RemoveItems( idx ); + for( var i = 0; i < tab.Mtrl.ShaderPackage.Samplers.Length; ++i ) { - if( file.ShaderPackage.Samplers[i].TextureIndex >= sampler.TextureIndex ) + if( tab.Mtrl.ShaderPackage.Samplers[ i ].TextureIndex >= sampler.TextureIndex ) { - --file.ShaderPackage.Samplers[i].TextureIndex; + --tab.Mtrl.ShaderPackage.Samplers[ i ].TextureIndex; } } @@ -559,20 +469,21 @@ public partial class ModEditWindow { foreach( var idx in orphanTextures ) { - ImRaii.TreeNode( $"#{idx}: {Path.GetFileName( file.Textures[idx].Path )} - {file.Textures[idx].Flags:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode( $"#{idx}: {Path.GetFileName( tab.Mtrl.Textures[ idx ].Path )} - {tab.Mtrl.Textures[ idx ].Flags:X4}", ImGuiTreeNodeFlags.Leaf ) + .Dispose(); } } } - else if( !disabled && file.AssociatedShpk != null && aliasedTextureCount == 0 && file.Textures.Length < 255 ) + else if( !disabled && tab.AssociatedShpk != null && aliasedTextureCount == 0 && tab.Mtrl.Textures.Length < 255 ) { - var missingSamplers = file.AssociatedShpk.Samplers.Where( sampler => sampler.Slot == 2 && !definedSamplers.Contains( sampler.Id ) ).ToArray(); + var missingSamplers = tab.AssociatedShpk.Samplers.Where( sampler => sampler.Slot == 2 && !definedSamplers.Contains( sampler.Id ) ).ToArray(); if( missingSamplers.Length > 0 ) { - var selectedSampler = Array.Find( missingSamplers, sampler => sampler.Id == _mtrlTabState.MaterialNewSamplerId ); + var selectedSampler = Array.Find( missingSamplers, sampler => sampler.Id == tab.MaterialNewSamplerId ); if( selectedSampler.Name == null ) { - selectedSampler = missingSamplers[0]; - _mtrlTabState.MaterialNewSamplerId = selectedSampler.Id; + selectedSampler = missingSamplers[ 0 ]; + tab.MaterialNewSamplerId = selectedSampler.Id; } ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 450.0f ); @@ -582,10 +493,10 @@ public partial class ModEditWindow { foreach( var sampler in missingSamplers ) { - if( ImGui.Selectable( $"{sampler.Name} (ID: 0x{sampler.Id:X8})", sampler.Id == _mtrlTabState.MaterialNewSamplerId ) ) + if( ImGui.Selectable( $"{sampler.Name} (ID: 0x{sampler.Id:X8})", sampler.Id == tab.MaterialNewSamplerId ) ) { - selectedSampler = sampler; - _mtrlTabState.MaterialNewSamplerId = sampler.Id; + selectedSampler = sampler; + tab.MaterialNewSamplerId = sampler.Id; } } } @@ -594,16 +505,16 @@ public partial class ModEditWindow ImGui.SameLine(); if( ImGui.Button( "Add Sampler" ) ) { - file.Textures = file.Textures.AddItem( new MtrlFile.Texture + tab.Mtrl.Textures = tab.Mtrl.Textures.AddItem( new MtrlFile.Texture { - Path = string.Empty, + Path = string.Empty, Flags = 0, } ); - file.ShaderPackage.Samplers = file.ShaderPackage.Samplers.AddItem( new Sampler + tab.Mtrl.ShaderPackage.Samplers = tab.Mtrl.ShaderPackage.Samplers.AddItem( new Sampler { - SamplerId = _mtrlTabState.MaterialNewSamplerId, - TextureIndex = ( byte )file.Textures.Length, - Flags = 0, + SamplerId = tab.MaterialNewSamplerId, + TextureIndex = ( byte )tab.Mtrl.Textures.Length, + Flags = 0, } ); ret = true; } @@ -615,7 +526,7 @@ public partial class ModEditWindow return ret; } - private bool DrawMaterialShaderResources( MtrlFile file, bool disabled ) + private bool DrawMaterialShaderResources( MtrlTab tab, bool disabled ) { var ret = false; if( !ImGui.CollapsingHeader( "Advanced Shader Resources" ) ) @@ -623,13 +534,13 @@ public partial class ModEditWindow return ret; } - ret |= DrawPackageNameInput( file, disabled ); - ret |= DrawShaderFlagsInput( file, disabled ); - DrawCustomAssociations( file ); - ret |= DrawMaterialShaderKeys( file, disabled ); - DrawMaterialShaders( file ); - ret |= DrawMaterialConstants( file, disabled ); - ret |= DrawMaterialSamplers( file, disabled ); + ret |= DrawPackageNameInput( tab, disabled ); + ret |= DrawShaderFlagsInput( tab.Mtrl, disabled ); + DrawCustomAssociations( tab ); + ret |= DrawMaterialShaderKeys( tab, disabled ); + DrawMaterialShaders( tab ); + ret |= DrawMaterialConstants( tab, disabled ); + ret |= DrawMaterialSamplers( tab, disabled ); return ret; } @@ -638,7 +549,7 @@ public partial class ModEditWindow { if( valueLength == 0 || valueOffset < 0 ) { - return (null, false); + return ( null, false ); } var firstVector = valueOffset >> 2; @@ -651,18 +562,18 @@ public partial class ModEditWindow if( firstVector == lastVector ) { - return ($"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, lastComponent + 1 - firstComponent )}", true); + return ( $"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, lastComponent + 1 - firstComponent )}", true ); } var parts = new string[lastVector + 1 - firstVector]; - parts[0] = $"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, 4 - firstComponent )}"; - parts[^1] = $"[{lastVector}]{VectorSwizzle( 0, lastComponent + 1 )}"; + parts[ 0 ] = $"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, 4 - firstComponent )}"; + parts[ ^1 ] = $"[{lastVector}]{VectorSwizzle( 0, lastComponent + 1 )}"; for( var i = firstVector + 1; i < lastVector; ++i ) { - parts[i - firstVector] = $"[{i}]"; + parts[ i - firstVector ] = $"[{i}]"; } - return (string.Join( ", ", parts ), false); + return ( string.Join( ", ", parts ), false ); } private static string? MaterialParamName( bool componentOnly, int offset ) @@ -672,7 +583,7 @@ public partial class ModEditWindow return null; } - var component = "xyzw"[offset & 0x3]; + var component = "xyzw"[ offset & 0x3 ]; return componentOnly ? new string( component, 1 ) : $"[{offset >> 2}].{component}"; } diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.cs index 3fac894b..2cf5cd26 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.cs @@ -1,152 +1,64 @@ -using System; -using System.Collections.Generic; using System.Linq; using System.Numerics; -using System.Runtime.InteropServices; using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.GameData.Files; using Penumbra.String.Classes; -using Penumbra.String.Functions; namespace Penumbra.UI.Classes; public partial class ModEditWindow { - private readonly FileEditor< MtrlFile > _materialTab; + private readonly FileEditor< MtrlTab > _materialTab; - private struct MtrlTabState + private bool DrawMaterialPanel( MtrlTab tab, bool disabled ) { - public uint MaterialNewKeyId = 0; - public uint MaterialNewConstantId = 0; - public uint MaterialNewSamplerId = 0; - - public readonly List< string > TextureLabels = new(4); - public FullPath LoadedShpkPath = FullPath.Empty; - public float TextureLabelWidth = 0f; - - public MtrlTabState() - { } - } - - private MtrlTabState _mtrlTabState = new(); - - - /// Load the material with an associated shader package if it can be found. See . - private MtrlFile LoadMtrl( byte[] bytes ) - { - var mtrl = new MtrlFile( bytes ); - LoadAssociatedShpk( mtrl ); - return mtrl; - } - - - private bool DrawMaterialPanel( MtrlFile file, bool disabled ) - { - var ret = DrawMaterialTextureChange( file, disabled ); + var ret = DrawMaterialTextureChange( tab, disabled ); ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawBackFaceAndTransparency( file, disabled ); + ret |= DrawBackFaceAndTransparency( tab.Mtrl, disabled ); ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawMaterialColorSetChange( file, disabled ); + ret |= DrawMaterialColorSetChange( tab.Mtrl, disabled ); ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawMaterialShaderResources( file, disabled ); + ret |= DrawMaterialShaderResources( tab, disabled ); ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawOtherMaterialDetails( file, disabled ); + DrawOtherMaterialDetails( tab.Mtrl, disabled ); _materialFileDialog.Draw(); return !disabled && ret; } - private bool DrawMaterialTextureChange( MtrlFile file, bool disabled ) + private static bool DrawMaterialTextureChange( MtrlTab tab, bool disabled ) { var ret = false; using var table = ImRaii.Table( "##Textures", 2 ); ImGui.TableSetupColumn( "Path", ImGuiTableColumnFlags.WidthStretch ); - ImGui.TableSetupColumn( "Name", ImGuiTableColumnFlags.WidthFixed, _mtrlTabState.TextureLabelWidth * ImGuiHelpers.GlobalScale ); - for( var i = 0; i < file.Textures.Length; ++i ) + ImGui.TableSetupColumn( "Name", ImGuiTableColumnFlags.WidthFixed, tab.TextureLabelWidth * ImGuiHelpers.GlobalScale ); + for( var i = 0; i < tab.Mtrl.Textures.Length; ++i ) { using var _ = ImRaii.PushId( i ); - var tmp = file.Textures[ i ].Path; + var tmp = tab.Mtrl.Textures[ i ].Path; ImGui.TableNextColumn(); ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X ); if( ImGui.InputText( string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) && tmp.Length > 0 - && tmp != file.Textures[ i ].Path ) + && tmp != tab.Mtrl.Textures[ i ].Path ) { ret = true; - file.Textures[ i ].Path = tmp; + tab.Mtrl.Textures[ i ].Path = tmp; } ImGui.TableNextColumn(); using var font = ImRaii.PushFont( UiBuilder.MonoFont ); ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( _mtrlTabState.TextureLabels[i] ); - } - - return ret; - } - - private static bool DrawMaterialColorSetChange( MtrlFile file, bool disabled ) - { - if( !file.ColorSets.Any( c => c.HasRows ) ) - { - return false; - } - - ColorSetCopyAllClipboardButton( file, 0 ); - ImGui.SameLine(); - var ret = ColorSetPasteAllClipboardButton( file, 0 ); - ImGui.SameLine(); - ImGui.Dummy( ImGuiHelpers.ScaledVector2( 20, 0 ) ); - ImGui.SameLine(); - ret |= DrawPreviewDye( file, disabled ); - - using var table = ImRaii.Table( "##ColorSets", 11, - ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV ); - if( !table ) - { - return false; - } - - ImGui.TableNextColumn(); - ImGui.TableHeader( string.Empty ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Row" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Diffuse" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Specular" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Emissive" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Gloss" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Tile" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Repeat" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Skew" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Dye" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Dye Preview" ); - - for( var j = 0; j < file.ColorSets.Length; ++j ) - { - using var _ = ImRaii.PushId( j ); - for( var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i ) - { - ret |= DrawColorSetRow( file, j, i, disabled ); - ImGui.TableNextRow(); - } + ImGui.TextUnformatted( tab.TextureLabels[ i ] ); } return ret; @@ -179,13 +91,11 @@ public partial class ModEditWindow return ret; } - private bool DrawOtherMaterialDetails( MtrlFile file, bool disabled ) + private static void DrawOtherMaterialDetails( MtrlFile file, bool _ ) { - var ret = false; - if( !ImGui.CollapsingHeader( "Further Content" ) ) { - return false; + return; } using( var sets = ImRaii.TreeNode( "UV Sets", ImGuiTreeNodeFlags.DefaultOpen ) ) @@ -199,378 +109,16 @@ public partial class ModEditWindow } } - if( file.AdditionalData.Length > 0 ) - { - using var t = ImRaii.TreeNode( $"Additional Data (Size: {file.AdditionalData.Length})###AdditionalData" ); - if( t ) - { - ImGuiUtil.TextWrapped( string.Join( ' ', file.AdditionalData.Select( c => $"{c:X2}" ) ) ); - } - } - - return ret; - } - - private static void ColorSetCopyAllClipboardButton( MtrlFile file, int colorSetIdx ) - { - if( !ImGui.Button( "Export All Rows to Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ) ) ) + if( file.AdditionalData.Length <= 0 ) { return; } - try + using var t = ImRaii.TreeNode( $"Additional Data (Size: {file.AdditionalData.Length})###AdditionalData" ); + if( t ) { - var data1 = file.ColorSets[ colorSetIdx ].Rows.AsBytes(); - var data2 = file.ColorDyeSets.Length > colorSetIdx ? file.ColorDyeSets[ colorSetIdx ].Rows.AsBytes() : ReadOnlySpan< byte >.Empty; - var array = new byte[data1.Length + data2.Length]; - data1.TryCopyTo( array ); - data2.TryCopyTo( array.AsSpan( data1.Length ) ); - var text = Convert.ToBase64String( array ); - ImGui.SetClipboardText( text ); + ImGuiUtil.TextWrapped( string.Join( ' ', file.AdditionalData.Select( c => $"{c:X2}" ) ) ); } - catch - { - // ignored - } - } - - private static bool DrawPreviewDye( MtrlFile file, bool disabled ) - { - var (dyeId, (name, dyeColor, _)) = Penumbra.StainManager.StainCombo.CurrentSelection; - var tt = dyeId == 0 ? "Select a preview dye first." : "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled."; - if( ImGuiUtil.DrawDisabledButton( "Apply Preview Dye", Vector2.Zero, tt, disabled || dyeId == 0 ) ) - { - var ret = false; - for( var j = 0; j < file.ColorDyeSets.Length; ++j ) - { - for( var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i ) - { - ret |= file.ApplyDyeTemplate( Penumbra.StainManager.StmFile, j, i, dyeId ); - } - } - - return ret; - } - - ImGui.SameLine(); - var label = dyeId == 0 ? "Preview Dye###previewDye" : $"{name} (Preview)###previewDye"; - Penumbra.StainManager.StainCombo.Draw( label, dyeColor, string.Empty, true ); - return false; - } - - private static unsafe bool ColorSetPasteAllClipboardButton( MtrlFile file, int colorSetIdx ) - { - if( !ImGui.Button( "Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ) ) || file.ColorSets.Length <= colorSetIdx ) - { - return false; - } - - try - { - var text = ImGui.GetClipboardText(); - var data = Convert.FromBase64String( text ); - if( data.Length < Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() ) - { - return false; - } - - ref var rows = ref file.ColorSets[ colorSetIdx ].Rows; - fixed( void* ptr = data, output = &rows ) - { - MemoryUtility.MemCpyUnchecked( output, ptr, Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() ); - if( data.Length >= Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() + Marshal.SizeOf< MtrlFile.ColorDyeSet.RowArray >() - && file.ColorDyeSets.Length > colorSetIdx ) - { - ref var dyeRows = ref file.ColorDyeSets[ colorSetIdx ].Rows; - fixed( void* output2 = &dyeRows ) - { - MemoryUtility.MemCpyUnchecked( output2, ( byte* )ptr + Marshal.SizeOf< MtrlFile.ColorSet.RowArray >(), Marshal.SizeOf< MtrlFile.ColorDyeSet.RowArray >() ); - } - } - } - - return true; - } - catch - { - return false; - } - } - - private static unsafe void ColorSetCopyClipboardButton( MtrlFile.ColorSet.Row row, MtrlFile.ColorDyeSet.Row dye ) - { - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Clipboard.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, - "Export this row to your clipboard.", false, true ) ) - { - try - { - var data = new byte[MtrlFile.ColorSet.Row.Size + 2]; - fixed( byte* ptr = data ) - { - MemoryUtility.MemCpyUnchecked( ptr, &row, MtrlFile.ColorSet.Row.Size ); - MemoryUtility.MemCpyUnchecked( ptr + MtrlFile.ColorSet.Row.Size, &dye, 2 ); - } - - var text = Convert.ToBase64String( data ); - ImGui.SetClipboardText( text ); - } - catch - { - // ignored - } - } - } - - private static unsafe bool ColorSetPasteFromClipboardButton( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled ) - { - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Paste.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, - "Import an exported row from your clipboard onto this row.", disabled, true ) ) - { - try - { - var text = ImGui.GetClipboardText(); - var data = Convert.FromBase64String( text ); - if( data.Length != MtrlFile.ColorSet.Row.Size + 2 - || file.ColorSets.Length <= colorSetIdx ) - { - return false; - } - - fixed( byte* ptr = data ) - { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ] = *( MtrlFile.ColorSet.Row* )ptr; - if( colorSetIdx < file.ColorDyeSets.Length ) - { - file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] = *( MtrlFile.ColorDyeSet.Row* )( ptr + MtrlFile.ColorSet.Row.Size ); - } - } - - return true; - } - catch - { - // ignored - } - } - - return false; - } - - private static bool DrawColorSetRow( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled ) - { - static bool FixFloat( ref float val, float current ) - { - val = ( float )( Half )val; - return val != current; - } - - using var id = ImRaii.PushId( rowIdx ); - var row = file.ColorSets[ colorSetIdx ].Rows[ rowIdx ]; - var hasDye = file.ColorDyeSets.Length > colorSetIdx; - var dye = hasDye ? file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] : new MtrlFile.ColorDyeSet.Row(); - var floatSize = 70 * ImGuiHelpers.GlobalScale; - var intSize = 45 * ImGuiHelpers.GlobalScale; - ImGui.TableNextColumn(); - ColorSetCopyClipboardButton( row, dye ); - ImGui.SameLine(); - var ret = ColorSetPasteFromClipboardButton( file, colorSetIdx, rowIdx, disabled ); - - ImGui.TableNextColumn(); - ImGui.TextUnformatted( $"#{rowIdx + 1:D2}" ); - - ImGui.TableNextColumn(); - using var dis = ImRaii.Disabled( disabled ); - ret |= ColorPicker( "##Diffuse", "Diffuse Color", row.Diffuse, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Diffuse = c ); - if( hasDye ) - { - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox( "##dyeDiffuse", "Apply Diffuse Color on Dye", dye.Diffuse, - b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Diffuse = b, ImGuiHoveredFlags.AllowWhenDisabled ); - } - - ImGui.TableNextColumn(); - ret |= ColorPicker( "##Specular", "Specular Color", row.Specular, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Specular = c ); - ImGui.SameLine(); - var tmpFloat = row.SpecularStrength; - ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##SpecularStrength", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.SpecularStrength ) ) - { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = tmpFloat; - ret = true; - } - - ImGuiUtil.HoverTooltip( "Specular Strength", ImGuiHoveredFlags.AllowWhenDisabled ); - - if( hasDye ) - { - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox( "##dyeSpecular", "Apply Specular Color on Dye", dye.Specular, - b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Specular = b, ImGuiHoveredFlags.AllowWhenDisabled ); - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox( "##dyeSpecularStrength", "Apply Specular Strength on Dye", dye.SpecularStrength, - b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = b, ImGuiHoveredFlags.AllowWhenDisabled ); - } - - ImGui.TableNextColumn(); - ret |= ColorPicker( "##Emissive", "Emissive Color", row.Emissive, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Emissive = c ); - if( hasDye ) - { - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox( "##dyeEmissive", "Apply Emissive Color on Dye", dye.Emissive, - b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Emissive = b, ImGuiHoveredFlags.AllowWhenDisabled ); - } - - ImGui.TableNextColumn(); - tmpFloat = row.GlossStrength; - ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##GlossStrength", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.GlossStrength ) ) - { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].GlossStrength = tmpFloat; - ret = true; - } - - ImGuiUtil.HoverTooltip( "Gloss Strength", ImGuiHoveredFlags.AllowWhenDisabled ); - if( hasDye ) - { - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox( "##dyeGloss", "Apply Gloss Strength on Dye", dye.Gloss, - b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Gloss = b, ImGuiHoveredFlags.AllowWhenDisabled ); - } - - ImGui.TableNextColumn(); - int tmpInt = row.TileSet; - ImGui.SetNextItemWidth( intSize ); - if( ImGui.InputInt( "##TileSet", ref tmpInt, 0, 0 ) && tmpInt != row.TileSet && tmpInt is >= 0 and <= ushort.MaxValue ) - { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].TileSet = ( ushort )tmpInt; - ret = true; - } - - ImGuiUtil.HoverTooltip( "Tile Set", ImGuiHoveredFlags.AllowWhenDisabled ); - - ImGui.TableNextColumn(); - tmpFloat = row.MaterialRepeat.X; - ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##RepeatX", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialRepeat.X ) ) - { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { X = tmpFloat }; - ret = true; - } - - ImGuiUtil.HoverTooltip( "Repeat X", ImGuiHoveredFlags.AllowWhenDisabled ); - ImGui.SameLine(); - tmpFloat = row.MaterialRepeat.Y; - ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##RepeatY", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialRepeat.Y ) ) - { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat }; - ret = true; - } - - ImGuiUtil.HoverTooltip( "Repeat Y", ImGuiHoveredFlags.AllowWhenDisabled ); - - ImGui.TableNextColumn(); - tmpFloat = row.MaterialSkew.X; - ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##SkewX", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialSkew.X ) ) - { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { X = tmpFloat }; - ret = true; - } - - ImGuiUtil.HoverTooltip( "Skew X", ImGuiHoveredFlags.AllowWhenDisabled ); - - ImGui.SameLine(); - tmpFloat = row.MaterialSkew.Y; - ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##SkewY", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialSkew.Y ) ) - { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { Y = tmpFloat }; - ret = true; - } - - ImGuiUtil.HoverTooltip( "Skew Y", ImGuiHoveredFlags.AllowWhenDisabled ); - - ImGui.TableNextColumn(); - if( hasDye ) - { - if( Penumbra.StainManager.TemplateCombo.Draw( "##dyeTemplate", dye.Template.ToString(), string.Empty, intSize - + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton ) ) - { - file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = Penumbra.StainManager.TemplateCombo.CurrentSelection; - ret = true; - } - - ImGuiUtil.HoverTooltip( "Dye Template", ImGuiHoveredFlags.AllowWhenDisabled ); - - ImGui.TableNextColumn(); - ret |= DrawDyePreview( file, colorSetIdx, rowIdx, disabled, dye, floatSize ); - } - else - { - ImGui.TableNextColumn(); - } - - - return ret; - } - - private static bool DrawDyePreview( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled, MtrlFile.ColorDyeSet.Row dye, float floatSize ) - { - var stain = Penumbra.StainManager.StainCombo.CurrentSelection.Key; - if( stain == 0 || !Penumbra.StainManager.StmFile.Entries.TryGetValue( dye.Template, out var entry ) ) - { - return false; - } - - var values = entry[ ( int )stain ]; - using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing / 2 ); - - var ret = ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2( ImGui.GetFrameHeight() ), - "Apply the selected dye to this row.", disabled, true ); - - ret = ret && file.ApplyDyeTemplate( Penumbra.StainManager.StmFile, colorSetIdx, rowIdx, stain ); - - ImGui.SameLine(); - ColorPicker( "##diffusePreview", string.Empty, values.Diffuse, _ => { }, "D" ); - ImGui.SameLine(); - ColorPicker( "##specularPreview", string.Empty, values.Specular, _ => { }, "S" ); - ImGui.SameLine(); - ColorPicker( "##emissivePreview", string.Empty, values.Emissive, _ => { }, "E" ); - ImGui.SameLine(); - using var dis = ImRaii.Disabled(); - ImGui.SetNextItemWidth( floatSize ); - ImGui.DragFloat( "##gloss", ref values.Gloss, 0, 0, 0, "%.2f G" ); - ImGui.SameLine(); - ImGui.SetNextItemWidth( floatSize ); - ImGui.DragFloat( "##specularStrength", ref values.SpecularPower, 0, 0, 0, "%.2f S" ); - - return ret; - } - - private static bool ColorPicker( string label, string tooltip, Vector3 input, Action< Vector3 > setter, string letter = "" ) - { - var ret = false; - var tmp = input; - if( ImGui.ColorEdit3( label, ref tmp, - ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.DisplayRGB | ImGuiColorEditFlags.InputRGB | ImGuiColorEditFlags.NoTooltip ) - && tmp != input ) - { - setter( tmp ); - ret = true; - } - - if( letter.Length > 0 && ImGui.IsItemVisible() ) - { - var textSize = ImGui.CalcTextSize( letter ); - var center = ImGui.GetItemRectMin() + ( ImGui.GetItemRectSize() - textSize ) / 2; - var textColor = input.LengthSquared() < 0.25f ? 0x80FFFFFFu : 0x80000000u; - ImGui.GetWindowDrawList().AddText( center, textColor, letter ); - } - - ImGuiUtil.HoverTooltip( tooltip, ImGuiHoveredFlags.AllowWhenDisabled ); - - return ret; } private void DrawMaterialReassignmentTab() diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index a3de3c34..9b0aca95 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -570,11 +570,11 @@ public partial class ModEditWindow : Window, IDisposable public ModEditWindow() : base( WindowBaseLabel ) { - _materialTab = new FileEditor< MtrlFile >( "Materials", ".mtrl", + _materialTab = new FileEditor< MtrlTab >( "Materials", ".mtrl", () => _editor?.MtrlFiles ?? Array.Empty< Editor.FileRegistry >(), DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, - LoadMtrl ); + bytes => new MtrlTab( this, new MtrlFile( bytes ) ) ); _modelTab = new FileEditor< MdlFile >( "Models", ".mdl", () => _editor?.MdlFiles ?? Array.Empty< Editor.FileRegistry >(), DrawModelPanel,