Material editor 2099

This commit is contained in:
Exter-N 2023-08-24 05:51:21 +02:00
parent f64fdd2b26
commit b8d09ab660
15 changed files with 1221 additions and 651 deletions

@ -1 +1 @@
Subproject commit 863d08bd83381bb7fe4a8d5c514f0ba55379336f
Subproject commit 1e172ee9a0f5946d67b848a36b2be97f6541453f

@ -1 +1 @@
Subproject commit 97643cad67b6981c3ee510d1ca12c4321e6a80bf
Subproject commit 07c001c5b2b35b2dba2b8428389d3ed375728616

View file

@ -190,7 +190,7 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
if (WithNames)
{
var name = samplers != null && i < samplers.Count ? samplers[i].Item2?.Name : null;
var name = samplers != null && i < samplers.Length ? samplers[i].ShpkSampler?.Name : null;
node.Children.Add(texNode.WithName(name ?? $"Texture #{i}"));
}
else

View file

@ -0,0 +1,15 @@
using System.Runtime.InteropServices;
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
namespace Penumbra.Interop.Structs;
[StructLayout( LayoutKind.Explicit )]
public unsafe struct CharacterBaseExt
{
[FieldOffset( 0x0 )]
public CharacterBase CharacterBase;
[FieldOffset( 0x258 )]
public Texture** ColorSetTextures;
}

View file

@ -9,6 +9,9 @@ public unsafe struct HumanExt
[FieldOffset( 0x0 )]
public Human Human;
[FieldOffset( 0x0 )]
public CharacterBaseExt CharacterBase;
[FieldOffset( 0x9E8 )]
public ResourceHandle* Decal;

View file

@ -13,22 +13,44 @@ namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow
{
private static readonly float HalfMinValue = (float)Half.MinValue;
private static readonly float HalfMaxValue = (float)Half.MaxValue;
private static readonly float HalfEpsilon = (float)Half.Epsilon;
private bool DrawMaterialColorSetChange( MtrlTab tab, bool disabled )
{
if( !tab.Mtrl.ColorSets.Any( c => c.HasRows ) )
if( !tab.SamplerIds.Contains( ShpkFile.TableSamplerId ) || !tab.Mtrl.ColorSets.Any( c => c.HasRows ) )
{
return false;
}
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
if( !ImGui.CollapsingHeader( "Color Set", ImGuiTreeNodeFlags.DefaultOpen ) )
{
return false;
}
var hasAnyDye = tab.UseColorDyeSet;
ColorSetCopyAllClipboardButton( tab.Mtrl, 0 );
ImGui.SameLine();
var ret = ColorSetPasteAllClipboardButton( tab, 0 );
ImGui.SameLine();
ImGui.Dummy( ImGuiHelpers.ScaledVector2( 20, 0 ) );
ImGui.SameLine();
ret |= DrawPreviewDye( tab, disabled );
var ret = ColorSetPasteAllClipboardButton( tab, 0, disabled );
if( !disabled )
{
ImGui.SameLine();
ImGui.Dummy( ImGuiHelpers.ScaledVector2( 20, 0 ) );
ImGui.SameLine();
ret |= ColorSetDyeableCheckbox( tab, ref hasAnyDye );
}
if( hasAnyDye )
{
ImGui.SameLine();
ImGui.Dummy( ImGuiHelpers.ScaledVector2( 20, 0 ) );
ImGui.SameLine();
ret |= DrawPreviewDye( tab, disabled );
}
using var table = ImRaii.Table( "##ColorSets", 11,
using var table = ImRaii.Table( "##ColorSets", hasAnyDye ? 11 : 9,
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV );
if( !table )
{
@ -53,17 +75,20 @@ public partial class ModEditWindow
ImGui.TableHeader( "Repeat" );
ImGui.TableNextColumn();
ImGui.TableHeader( "Skew" );
ImGui.TableNextColumn();
ImGui.TableHeader( "Dye" );
ImGui.TableNextColumn();
ImGui.TableHeader( "Dye Preview" );
if( hasAnyDye )
{
ImGui.TableNextColumn();
ImGui.TableHeader("Dye");
ImGui.TableNextColumn();
ImGui.TableHeader("Dye Preview");
}
for( var j = 0; j < tab.Mtrl.ColorSets.Length; ++j )
{
using var _ = ImRaii.PushId( j );
for( var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i )
{
ret |= DrawColorSetRow( tab, j, i, disabled );
ret |= DrawColorSetRow( tab, j, i, disabled, hasAnyDye );
ImGui.TableNextRow();
}
}
@ -122,9 +147,9 @@ public partial class ModEditWindow
return false;
}
private static unsafe bool ColorSetPasteAllClipboardButton( MtrlTab tab, int colorSetIdx )
private static unsafe bool ColorSetPasteAllClipboardButton( MtrlTab tab, int colorSetIdx, bool disabled )
{
if( !ImGui.Button( "Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ) ) || tab.Mtrl.ColorSets.Length <= colorSetIdx )
if( !ImGuiUtil.DrawDisabledButton( "Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ), string.Empty, disabled ) || tab.Mtrl.ColorSets.Length <= colorSetIdx )
{
return false;
}
@ -187,6 +212,21 @@ public partial class ModEditWindow
}
}
private static bool ColorSetDyeableCheckbox( MtrlTab tab, ref bool dyeable )
{
var ret = ImGui.Checkbox( "Dyeable", ref dyeable );
if( ret )
{
tab.UseColorDyeSet = dyeable;
if( dyeable )
tab.Mtrl.FindOrAddColorDyeSet();
tab.UpdateColorSetPreview();
}
return ret;
}
private static unsafe bool ColorSetPasteFromClipboardButton( MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled )
{
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Paste.ToIconString(), ImGui.GetFrameHeight() * Vector2.One,
@ -235,7 +275,7 @@ public partial class ModEditWindow
tab.CancelColorSetHighlight();
}
private bool DrawColorSetRow( MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled )
private bool DrawColorSetRow( MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled, bool hasAnyDye )
{
static bool FixFloat( ref float val, float current )
{
@ -245,7 +285,7 @@ public partial class ModEditWindow
using var id = ImRaii.PushId( rowIdx );
var row = tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ];
var hasDye = tab.Mtrl.ColorDyeSets.Length > colorSetIdx;
var hasDye = hasAnyDye && tab.Mtrl.ColorDyeSets.Length > colorSetIdx;
var dye = hasDye ? tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] : new MtrlFile.ColorDyeSet.Row();
var floatSize = 70 * UiHelpers.Scale;
var intSize = 45 * UiHelpers.Scale;
@ -274,7 +314,7 @@ public partial class ModEditWindow
ImGui.SameLine();
var tmpFloat = row.SpecularStrength;
ImGui.SetNextItemWidth( floatSize );
if( ImGui.DragFloat( "##SpecularStrength", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.SpecularStrength ) )
if( ImGui.DragFloat( "##SpecularStrength", ref tmpFloat, 0.1f, 0f, HalfMaxValue, "%.2f" ) && FixFloat( ref tmpFloat, row.SpecularStrength ) )
{
tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = tmpFloat;
ret = true;
@ -305,9 +345,9 @@ public partial class ModEditWindow
ImGui.TableNextColumn();
tmpFloat = row.GlossStrength;
ImGui.SetNextItemWidth( floatSize );
if( ImGui.DragFloat( "##GlossStrength", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.GlossStrength ) )
if( ImGui.DragFloat( "##GlossStrength", ref tmpFloat, Math.Max( 0.1f, tmpFloat * 0.025f ), HalfEpsilon, HalfMaxValue, "%.1f" ) && FixFloat( ref tmpFloat, row.GlossStrength ) )
{
tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].GlossStrength = tmpFloat;
tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].GlossStrength = Math.Max(tmpFloat, HalfEpsilon);
ret = true;
tab.UpdateColorSetRowPreview(rowIdx);
}
@ -323,9 +363,9 @@ public partial class ModEditWindow
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 )
if( ImGui.DragInt( "##TileSet", ref tmpInt, 0.25f, 0, 63 ) && tmpInt != row.TileSet && tmpInt is >= 0 and <= ushort.MaxValue )
{
tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].TileSet = ( ushort )tmpInt;
tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].TileSet = ( ushort )Math.Clamp(tmpInt, 0, 63);
ret = true;
tab.UpdateColorSetRowPreview(rowIdx);
}
@ -335,7 +375,7 @@ public partial class ModEditWindow
ImGui.TableNextColumn();
tmpFloat = row.MaterialRepeat.X;
ImGui.SetNextItemWidth( floatSize );
if( ImGui.DragFloat( "##RepeatX", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialRepeat.X ) )
if( ImGui.DragFloat( "##RepeatX", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f" ) && FixFloat( ref tmpFloat, row.MaterialRepeat.X ) )
{
tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { X = tmpFloat };
ret = true;
@ -346,7 +386,7 @@ public partial class ModEditWindow
ImGui.SameLine();
tmpFloat = row.MaterialRepeat.Y;
ImGui.SetNextItemWidth( floatSize );
if( ImGui.DragFloat( "##RepeatY", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialRepeat.Y ) )
if( ImGui.DragFloat( "##RepeatY", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f" ) && FixFloat( ref tmpFloat, row.MaterialRepeat.Y ) )
{
tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat };
ret = true;
@ -358,7 +398,7 @@ public partial class ModEditWindow
ImGui.TableNextColumn();
tmpFloat = row.MaterialSkew.X;
ImGui.SetNextItemWidth( floatSize );
if( ImGui.DragFloat( "##SkewX", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialSkew.X ) )
if( ImGui.DragFloat( "##SkewX", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f" ) && FixFloat( ref tmpFloat, row.MaterialSkew.X ) )
{
tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { X = tmpFloat };
ret = true;
@ -370,7 +410,7 @@ public partial class ModEditWindow
ImGui.SameLine();
tmpFloat = row.MaterialSkew.Y;
ImGui.SetNextItemWidth( floatSize );
if( ImGui.DragFloat( "##SkewY", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialSkew.Y ) )
if( ImGui.DragFloat( "##SkewY", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f" ) && FixFloat( ref tmpFloat, row.MaterialSkew.Y ) )
{
tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { Y = tmpFloat };
ret = true;
@ -379,10 +419,10 @@ public partial class ModEditWindow
ImGuiUtil.HoverTooltip( "Skew Y", ImGuiHoveredFlags.AllowWhenDisabled );
ImGui.TableNextColumn();
if( hasDye )
{
if(_stainService.TemplateCombo.Draw( "##dyeTemplate", dye.Template.ToString(), string.Empty, intSize
ImGui.TableNextColumn();
if (_stainService.TemplateCombo.Draw( "##dyeTemplate", dye.Template.ToString(), string.Empty, intSize
+ ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton ) )
{
tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = _stainService.TemplateCombo.CurrentSelection;
@ -395,9 +435,10 @@ public partial class ModEditWindow
ImGui.TableNextColumn();
ret |= DrawDyePreview( tab, colorSetIdx, rowIdx, disabled, dye, floatSize );
}
else
else if ( hasAnyDye )
{
ImGui.TableNextColumn();
ImGui.TableNextColumn();
}
@ -431,10 +472,10 @@ public partial class ModEditWindow
ImGui.SameLine();
using var dis = ImRaii.Disabled();
ImGui.SetNextItemWidth( floatSize );
ImGui.DragFloat( "##gloss", ref values.Gloss, 0, 0, 0, "%.2f G" );
ImGui.DragFloat( "##gloss", ref values.Gloss, 0, values.Gloss, values.Gloss, "%.1f G" );
ImGui.SameLine();
ImGui.SetNextItemWidth( floatSize );
ImGui.DragFloat( "##specularStrength", ref values.SpecularPower, 0, 0, 0, "%.2f S" );
ImGui.DragFloat( "##specularStrength", ref values.SpecularPower, 0, values.SpecularPower, values.SpecularPower, "%.2f S" );
return ret;
}

View file

@ -0,0 +1,235 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using ImGuiNET;
using OtterGui.Raii;
using OtterGui;
using Penumbra.GameData;
namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow
{
private interface IConstantEditor
{
bool Draw(Span<float> values, bool disabled, float editorWidth);
}
private sealed class FloatConstantEditor : IConstantEditor
{
public static readonly FloatConstantEditor Default = new(null, null, 0.1f, 0.0f, 1.0f, 0.0f, 3, string.Empty);
private readonly float? _minimum;
private readonly float? _maximum;
private readonly float _speed;
private readonly float _relativeSpeed;
private readonly float _factor;
private readonly float _bias;
private readonly string _format;
public FloatConstantEditor(float? minimum, float? maximum, float speed, float relativeSpeed, float factor, float bias, byte precision, string unit)
{
_minimum = minimum;
_maximum = maximum;
_speed = speed;
_relativeSpeed = relativeSpeed;
_factor = factor;
_bias = bias;
_format = $"%.{Math.Min(precision, (byte)9)}f";
if (unit.Length > 0)
_format = $"{_format} {unit.Replace("%", "%%")}";
}
public bool Draw(Span<float> values, bool disabled, float editorWidth)
{
var fieldWidth = (editorWidth - (values.Length - 1) * ImGui.GetStyle().ItemSpacing.X) / values.Length;
var ret = false;
for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx)
{
if (valueIdx > 0)
ImGui.SameLine();
ImGui.SetNextItemWidth(MathF.Round(fieldWidth * (valueIdx + 1)) - MathF.Round(fieldWidth * valueIdx));
var value = (values[valueIdx] - _bias) / _factor;
if (disabled)
ImGui.DragFloat($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), value, value, _format);
else
{
if (ImGui.DragFloat($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), _minimum ?? 0.0f, _maximum ?? 0.0f, _format))
{
values[valueIdx] = Clamp(value) * _factor + _bias;
ret = true;
}
}
}
return ret;
}
private float Clamp(float value)
=> Math.Clamp(value, _minimum ?? float.NegativeInfinity, _maximum ?? float.PositiveInfinity);
}
private sealed class IntConstantEditor : IConstantEditor
{
private readonly int? _minimum;
private readonly int? _maximum;
private readonly float _speed;
private readonly float _relativeSpeed;
private readonly float _factor;
private readonly float _bias;
private readonly string _format;
public IntConstantEditor(int? minimum, int? maximum, float speed, float relativeSpeed, float factor, float bias, string unit)
{
_minimum = minimum;
_maximum = maximum;
_speed = speed;
_relativeSpeed = relativeSpeed;
_factor = factor;
_bias = bias;
_format = "%d";
if (unit.Length > 0)
_format = $"{_format} {unit.Replace("%", "%%")}";
}
public bool Draw(Span<float> values, bool disabled, float editorWidth)
{
var fieldWidth = (editorWidth - (values.Length - 1) * ImGui.GetStyle().ItemSpacing.X) / values.Length;
var ret = false;
for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx)
{
if (valueIdx > 0)
ImGui.SameLine();
ImGui.SetNextItemWidth(MathF.Round(fieldWidth * (valueIdx + 1)) - MathF.Round(fieldWidth * valueIdx));
var value = (int)Math.Clamp(MathF.Round((values[valueIdx] - _bias) / _factor), int.MinValue, int.MaxValue);
if (disabled)
ImGui.DragInt($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), value, value, _format);
else
{
if (ImGui.DragInt($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), _minimum ?? 0, _maximum ?? 0, _format))
{
values[valueIdx] = Clamp(value) * _factor + _bias;
ret = true;
}
}
}
return ret;
}
private int Clamp(int value)
=> Math.Clamp(value, _minimum ?? int.MinValue, _maximum ?? int.MaxValue);
}
private sealed class ColorConstantEditor : IConstantEditor
{
private readonly bool _squaredRgb;
private readonly bool _clamped;
public ColorConstantEditor(bool squaredRgb, bool clamped)
{
_squaredRgb = squaredRgb;
_clamped = clamped;
}
public bool Draw(Span<float> values, bool disabled, float editorWidth)
{
if (values.Length == 3)
{
ImGui.SetNextItemWidth(editorWidth);
var value = new Vector3(values);
if (_squaredRgb)
value = Vector3.SquareRoot(value);
if (ImGui.ColorEdit3("##0", ref value) && !disabled)
{
if (_squaredRgb)
value *= value;
if (_clamped)
value = Vector3.Clamp(value, Vector3.Zero, Vector3.One);
value.CopyTo(values);
return true;
}
return false;
}
else if (values.Length == 4)
{
ImGui.SetNextItemWidth(editorWidth);
var value = new Vector4(values);
if (_squaredRgb)
value = new Vector4(MathF.Sqrt(value.X), MathF.Sqrt(value.Y), MathF.Sqrt(value.Z), value.W);
if (ImGui.ColorEdit4("##0", ref value) && !disabled)
{
if (_squaredRgb)
value *= new Vector4(value.X, value.Y, value.Z, 1.0f);
if (_clamped)
value = Vector4.Clamp(value, Vector4.Zero, Vector4.One);
value.CopyTo(values);
return true;
}
return false;
}
else
return FloatConstantEditor.Default.Draw(values, disabled, editorWidth);
}
}
private sealed class EnumConstantEditor : IConstantEditor
{
private readonly IReadOnlyList<(string Label, float Value, string Description)> _values;
public EnumConstantEditor(IReadOnlyList<(string Label, float Value, string Description)> values)
{
_values = values;
}
public bool Draw(Span<float> values, bool disabled, float editorWidth)
{
var fieldWidth = (editorWidth - (values.Length - 1) * ImGui.GetStyle().ItemSpacing.X) / values.Length;
var ret = false;
for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx)
{
if (valueIdx > 0)
ImGui.SameLine();
ImGui.SetNextItemWidth(MathF.Round(fieldWidth * (valueIdx + 1)) - MathF.Round(fieldWidth * valueIdx));
var currentValue = values[valueIdx];
var (currentLabel, _, currentDescription) = _values.FirstOrNull(v => v.Value == currentValue) ?? (currentValue.ToString(), currentValue, string.Empty);
if (disabled)
ImGui.InputText($"##{valueIdx}", ref currentLabel, (uint)currentLabel.Length, ImGuiInputTextFlags.ReadOnly);
else
{
using var c = ImRaii.Combo($"##{valueIdx}", currentLabel);
{
if (c)
foreach (var (valueLabel, value, valueDescription) in _values)
{
if (ImGui.Selectable(valueLabel, value == currentValue))
{
values[valueIdx] = value;
ret = true;
}
if (valueDescription.Length > 0)
ImGuiUtil.SelectableHelpMarker(valueDescription);
}
}
}
}
return ret;
}
}
}

View file

@ -398,7 +398,7 @@ public partial class ModEditWindow
if (mtrlHandle == null)
throw new InvalidOperationException("Material doesn't have a resource handle");
var colorSetTextures = *(Texture***)((nint)DrawObject + 0x258);
var colorSetTextures = ((Structs.CharacterBaseExt*)DrawObject)->ColorSetTextures;
if (colorSetTextures == null)
throw new InvalidOperationException("Draw object doesn't have color set textures");
@ -424,7 +424,8 @@ public partial class ModEditWindow
if (reset)
{
var oldTexture = (Texture*)Interlocked.Exchange(ref *(nint*)_colorSetTexture, (nint)_originalColorSetTexture);
Structs.TextureUtility.DecRef(oldTexture);
if (oldTexture != null)
Structs.TextureUtility.DecRef(oldTexture);
}
else
Structs.TextureUtility.DecRef(_originalColorSetTexture);
@ -460,7 +461,8 @@ public partial class ModEditWindow
if (success)
{
var oldTexture = (Texture*)Interlocked.Exchange(ref *(nint*)_colorSetTexture, (nint)newTexture);
Structs.TextureUtility.DecRef(oldTexture);
if (oldTexture != null)
Structs.TextureUtility.DecRef(oldTexture);
}
else
Structs.TextureUtility.DecRef(newTexture);
@ -471,7 +473,7 @@ public partial class ModEditWindow
if (!base.IsStillValid())
return false;
var colorSetTextures = *(Texture***)((nint)DrawObject + 0x258);
var colorSetTextures = ((Structs.CharacterBaseExt*)DrawObject)->ColorSetTextures;
if (colorSetTextures == null)
return false;

View file

@ -8,6 +8,7 @@ using Dalamud.Interface.Internal.Notifications;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using ImGuiNET;
using Newtonsoft.Json.Linq;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii;
@ -16,6 +17,7 @@ using Penumbra.GameData.Data;
using Penumbra.GameData.Files;
using Penumbra.GameData.Structs;
using Penumbra.Services;
using Penumbra.String;
using Penumbra.String.Classes;
using Penumbra.Util;
using static Penumbra.GameData.Files.ShpkFile;
@ -26,49 +28,47 @@ public partial class ModEditWindow
{
private sealed class MtrlTab : IWritable, IDisposable
{
private const int ShpkPrefixLength = 16;
private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk/"u8, true, true, true);
private readonly ModEditWindow _edit;
public readonly MtrlFile Mtrl;
public readonly string FilePath;
public readonly bool Writable;
public uint NewKeyId;
public uint NewKeyDefault;
public uint NewConstantId;
public int NewConstantIdx;
public uint NewSamplerId;
public int NewSamplerIdx;
private string[]? _shpkNames;
public string ShaderHeader = "Shader###Shader";
public FullPath LoadedShpkPath = FullPath.Empty;
public string LoadedShpkPathName = string.Empty;
public string LoadedShpkDevkitPathName = string.Empty;
public string ShaderComment = string.Empty;
public ShpkFile? AssociatedShpk;
public JObject? AssociatedShpkDevkit;
public ShpkFile? AssociatedShpk;
public readonly List< string > TextureLabels = new(4);
public FullPath LoadedShpkPath = FullPath.Empty;
public string LoadedShpkPathName = string.Empty;
public float TextureLabelWidth;
public readonly string LoadedBaseDevkitPathName = string.Empty;
public readonly JObject? AssociatedBaseDevkit;
// 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 readonly List< (string Label, int Index, string Description, bool MonoFont, IReadOnlyList< (string Label, uint Value, string Description) > Values) > ShaderKeys = new(16);
public readonly HashSet< int > VertexShaders = new(16);
public readonly HashSet< int > PixelShaders = new(16);
public bool ShadersKnown = false;
public string VertexShadersString = "Vertex Shaders: ???";
public string PixelShadersString = "Pixel Shaders: ???";
// Textures & Samplers
public readonly List< (string Label, int TextureIndex, int SamplerIndex, string Description, bool MonoFont) > Textures = new(4);
public readonly HashSet< int > UnfoldedTextures = new(4);
public readonly HashSet< uint > SamplerIds = new(16);
public float TextureLabelWidth;
public bool UseColorDyeSet;
// Material Constants
public readonly List< (string Name, bool ComponentOnly, int ParamValueOffset) > MaterialConstants = new(16);
public readonly List< (string Name, uint Id, ushort ByteSize) > MissingMaterialConstants = new(16);
public readonly HashSet< uint > DefinedMaterialConstants = new(16);
public string MaterialConstantLabel = "Constants###Constants";
public IndexSet OrphanedMaterialValues = new(0, false);
public int AliasedMaterialValueCount;
public bool HasMalformedMaterialConstants;
// Samplers
public readonly List< (string Label, string FileName, uint Id) > Samplers = new(4);
public readonly List< (string Name, uint Id) > MissingSamplers = new(4);
public readonly HashSet< uint > DefinedSamplers = new(4);
public IndexSet OrphanedSamplers = new(0, false);
public int AliasedSamplerCount;
public readonly List< (string Header, List< (string Label, int ConstantIndex, Range Slice, string Description, bool MonoFont, IConstantEditor Editor) > Constants) > Constants = new(16);
// Live-Previewers
public readonly List<LiveMaterialPreviewer> MaterialPreviewers = new(4);
@ -87,8 +87,24 @@ public partial class ModEditWindow
return _edit.FindBestMatch( defaultGamePath );
}
public string[] GetShpkNames()
{
if (null != _shpkNames)
return _shpkNames;
var names = new HashSet<string>(StandardShaderPackages);
names.UnionWith(_edit.FindPathsStartingWith(ShpkPrefix).Select(path => path.ToString()[ShpkPrefixLength..]));
_shpkNames = names.ToArray();
Array.Sort(_shpkNames);
return _shpkNames;
}
public void LoadShpk( FullPath path )
{
ShaderHeader = $"Shader ({Mtrl.ShaderPackage.Name})###Shader";
try
{
LoadedShpkPath = path;
@ -106,180 +122,314 @@ public partial class ModEditWindow
Penumbra.Chat.NotificationMessage( $"Could not load {LoadedShpkPath.ToPath()}:\n{e}", "Penumbra Advanced Editing", NotificationType.Error );
}
if( LoadedShpkPath.InternalName.IsEmpty )
{
AssociatedShpkDevkit = null;
LoadedShpkDevkitPathName = string.Empty;
}
else
AssociatedShpkDevkit = TryLoadShpkDevkit( Path.GetFileNameWithoutExtension( Mtrl.ShaderPackage.Name ), out LoadedShpkDevkitPathName );
UpdateShaderKeys();
Update();
}
public void UpdateTextureLabels()
private JObject? TryLoadShpkDevkit(string shpkBaseName, out string devkitPathName)
{
var samplers = Mtrl.GetSamplersByTexture( AssociatedShpk );
TextureLabels.Clear();
TextureLabelWidth = 50f * UiHelpers.Scale;
using( var _ = ImRaii.PushFont( UiBuilder.MonoFont ) )
try
{
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 );
}
}
if (!Utf8GamePath.FromString("penumbra/shpk_devkit/" + shpkBaseName + ".json", out var devkitPath))
throw new Exception("Could not assemble ShPk dev-kit path.");
TextureLabelWidth = TextureLabelWidth / UiHelpers.Scale + 4;
var devkitFullPath = _edit.FindBestMatch(devkitPath);
if (!devkitFullPath.IsRooted)
throw new Exception("Could not resolve ShPk dev-kit path.");
devkitPathName = devkitFullPath.FullName;
return JObject.Parse(File.ReadAllText(devkitFullPath.FullName));
}
catch
{
devkitPathName = string.Empty;
return null;
}
}
public void UpdateShaderKeyLabels()
private T? TryGetShpkDevkitData<T>(string category, uint? id, bool mayVary) where T : class
{
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 );
}
return TryGetShpkDevkitData<T>(AssociatedShpkDevkit, LoadedShpkDevkitPathName, category, id, mayVary)
?? TryGetShpkDevkitData<T>(AssociatedBaseDevkit, LoadedBaseDevkitPathName, category, id, mayVary);
}
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() );
private T? TryGetShpkDevkitData<T>(JObject? devkit, string devkitPathName, string category, uint? id, bool mayVary) where T : class
{
if (devkit == null)
return null;
if( MissingShaderKeyIndices.Count > 0 && MissingShaderKeyIndices.All( i => AssociatedShpk.MaterialKeys[ i ].Id != NewKeyId ) )
try
{
var data = devkit[category];
if (id.HasValue)
data = data?[id.Value.ToString()];
if (mayVary && (data as JObject)?["Vary"] != null)
{
var key = AssociatedShpk.MaterialKeys[ MissingShaderKeyIndices[ 0 ] ];
NewKeyId = key.Id;
NewKeyDefault = key.DefaultValue;
var selector = BuildSelector(data!["Vary"]!
.Select(key => (uint)key)
.Select(key => Mtrl.GetShaderKey(key)?.Value ?? AssociatedShpk!.GetMaterialKeyById(key)!.Value.DefaultValue));
var index = (int)data["Selectors"]![selector.ToString()]!;
data = data["Items"]![index];
}
AvailableKeyValues.AddRange( AssociatedShpk.MaterialKeys.Select( k => DefinedShaderKeys.TryGetValue( k.Id, out var value ) ? value : k.DefaultValue ) );
foreach( var node in AssociatedShpk.Nodes )
return data?.ToObject(typeof(T)) as T;
}
catch (Exception e)
{
// Some element in the JSON was undefined or invalid (wrong type, key that doesn't exist in the ShPk, index out of range, …)
Penumbra.Log.Error($"Error while traversing the ShPk dev-kit file at {devkitPathName}: {e}");
return null;
}
}
public void UpdateShaderKeys()
{
ShaderKeys.Clear();
if (AssociatedShpk != null)
{
foreach (var key in AssociatedShpk.MaterialKeys)
{
if( node.MaterialKeys.WithIndex().All( key => key.Value == AvailableKeyValues[ key.Index ] ) )
var dkData = TryGetShpkDevkitData<DevkitShaderKey>("ShaderKeys", key.Id, false);
var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label);
var valueSet = new HashSet<uint>(key.Values);
if (dkData != null)
valueSet.UnionWith(dkData.Values.Keys);
var mtrlKeyIndex = Mtrl.FindOrAddShaderKey(key.Id, key.DefaultValue);
var values = valueSet.Select<uint, (string Label, uint Value, string Description)>(value =>
{
foreach( var pass in node.Passes )
if (dkData != null && dkData.Values.TryGetValue(value, out var dkValue))
return (dkValue.Label.Length > 0 ? dkValue.Label : $"0x{value:X8}", value, dkValue.Description);
else
return ($"0x{value:X8}", value, string.Empty);
}).ToArray();
Array.Sort(values, (x, y) =>
{
if (x.Value == key.DefaultValue)
return -1;
if (y.Value == key.DefaultValue)
return 1;
return x.Label.CompareTo(y.Label);
});
ShaderKeys.Add((hasDkLabel ? dkData!.Label : $"0x{key.Id:X8}", mtrlKeyIndex, dkData?.Description ?? string.Empty, !hasDkLabel, values));
}
}
else
{
foreach (var (key, index) in Mtrl.ShaderPackage.ShaderKeys.WithIndex())
ShaderKeys.Add(($"0x{key.Category:X8}", index, string.Empty, true, Array.Empty<(string, uint, string)>()));
}
}
public void UpdateShaders()
{
VertexShaders.Clear();
PixelShaders.Clear();
if (AssociatedShpk == null)
ShadersKnown = false;
else
{
ShadersKnown = true;
var systemKeySelectors = AllSelectors(AssociatedShpk.SystemKeys).ToArray();
var sceneKeySelectors = AllSelectors(AssociatedShpk.SceneKeys).ToArray();
var subViewKeySelectors = AllSelectors(AssociatedShpk.SubViewKeys).ToArray();
var materialKeySelector = BuildSelector(AssociatedShpk.MaterialKeys.Select(key => Mtrl.GetOrAddShaderKey(key.Id, key.DefaultValue).Value));
foreach (var systemKeySelector in systemKeySelectors)
{
foreach (var sceneKeySelector in sceneKeySelectors)
{
foreach (var subViewKeySelector in subViewKeySelectors)
{
vertexShaders.Add( ( int )pass.VertexShader );
pixelShaders.Add( ( int )pass.PixelShader );
var selector = BuildSelector(systemKeySelector, sceneKeySelector, materialKeySelector, subViewKeySelector);
var node = AssociatedShpk.GetNodeBySelector(selector);
if (node.HasValue)
{
foreach (var pass in node.Value.Passes)
{
VertexShaders.Add((int)pass.VertexShader);
PixelShaders.Add((int)pass.PixelShader);
}
}
else
ShadersKnown = false;
}
}
}
}
VertexShaders = $"Vertex Shaders: {( vertexShaders.Count > 0 ? string.Join( ", ", vertexShaders.Select( i => $"#{i}" ) ) : "???" )}";
PixelShaders = $"Pixel Shaders: {( pixelShaders.Count > 0 ? string.Join( ", ", pixelShaders.Select( i => $"#{i}" ) ) : "???" )}";
var vertexShaders = VertexShaders.OrderBy(i => i).Select(i => $"#{i}");
var pixelShaders = PixelShaders.OrderBy(i => i).Select(i => $"#{i}");
VertexShadersString = $"Vertex Shaders: {string.Join(", ", ShadersKnown ? vertexShaders : vertexShaders.Append("???"))}";
PixelShadersString = $"Pixel Shaders: {string.Join(", ", ShadersKnown ? pixelShaders : pixelShaders.Append("???"))}";
ShaderComment = TryGetShpkDevkitData<string>("Comment", null, true) ?? string.Empty;
}
public void UpdateConstantLabels()
public void UpdateTextures()
{
var prefix = AssociatedShpk?.GetConstantById( MaterialParamsConstantId )?.Name ?? string.Empty;
MaterialConstantLabel = prefix.Length == 0 ? "Constants###Constants" : prefix + "###Constants";
DefinedMaterialConstants.Clear();
MaterialConstants.Clear();
HasMalformedMaterialConstants = false;
AliasedMaterialValueCount = 0;
OrphanedMaterialValues = new IndexSet( Mtrl.ShaderPackage.ShaderValues.Length, true );
foreach( var (constant, idx) in Mtrl.ShaderPackage.Constants.WithIndex() )
Textures.Clear();
SamplerIds.Clear();
if (AssociatedShpk == null)
{
DefinedMaterialConstants.Add( constant.Id );
var values = Mtrl.GetConstantValues( constant );
var paramValueOffset = -values.Length;
if( values.Length > 0 )
SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId));
if (Mtrl.ColorSets.Any(c => c.HasRows))
SamplerIds.Add(TableSamplerId);
foreach (var (sampler, index) in Mtrl.ShaderPackage.Samplers.WithIndex())
Textures.Add(($"0x{sampler.SamplerId:X8}", sampler.TextureIndex, index, string.Empty, true));
}
else
{
foreach (var index in VertexShaders)
SamplerIds.UnionWith(AssociatedShpk.VertexShaders[index].Samplers.Select(sampler => sampler.Id));
foreach (var index in PixelShaders)
SamplerIds.UnionWith(AssociatedShpk.PixelShaders[index].Samplers.Select(sampler => sampler.Id));
if (!ShadersKnown)
{
var shpkParam = AssociatedShpk?.GetMaterialParamById( constant.Id );
var paramByteOffset = shpkParam?.ByteOffset ?? -1;
if( ( paramByteOffset & 0x3 ) == 0 )
{
paramValueOffset = paramByteOffset >> 2;
}
var unique = OrphanedMaterialValues.RemoveRange( constant.ByteOffset >> 2, values.Length );
AliasedMaterialValueCount += values.Length - unique;
SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId));
if (Mtrl.ColorSets.Any(c => c.HasRows))
SamplerIds.Add(TableSamplerId);
}
else
foreach (var samplerId in SamplerIds)
{
HasMalformedMaterialConstants = true;
var shpkSampler = AssociatedShpk.GetSamplerById(samplerId);
if (!shpkSampler.HasValue || shpkSampler.Value.Slot != 2)
continue;
var dkData = TryGetShpkDevkitData<DevkitSampler>("Samplers", samplerId, true);
var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label);
var sampler = Mtrl.GetOrAddSampler(samplerId, dkData?.DefaultTexture ?? string.Empty, out var samplerIndex);
Textures.Add((hasDkLabel ? dkData!.Label : shpkSampler.Value.Name, sampler.TextureIndex, samplerIndex, dkData?.Description ?? string.Empty, !hasDkLabel));
}
if (SamplerIds.Contains(TableSamplerId))
Mtrl.FindOrAddColorSet();
}
Textures.Sort((x, y) => string.CompareOrdinal(x.Label, y.Label));
var (name, componentOnly) = MaterialParamRangeName( prefix, paramValueOffset, values.Length );
var label = name == null
? $"#{idx:D2} (ID: 0x{constant.Id:X8})###{constant.Id}"
: $"#{idx:D2}: {name} (ID: 0x{constant.Id:X8})###{constant.Id}";
TextureLabelWidth = 50f * UiHelpers.Scale;
MaterialConstants.Add( ( label, componentOnly, paramValueOffset ) );
float helpWidth;
using (var _ = ImRaii.PushFont(UiBuilder.IconFont))
helpWidth = ImGui.GetStyle().ItemSpacing.X + ImGui.CalcTextSize(FontAwesomeIcon.InfoCircle.ToIconString()).X;
foreach (var (label, _, _, description, monoFont) in Textures)
if (!monoFont)
TextureLabelWidth = Math.Max(TextureLabelWidth, ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f));
using (var _ = ImRaii.PushFont(UiBuilder.MonoFont))
{
foreach (var (label, _, _, description, monoFont) in Textures)
if (monoFont)
TextureLabelWidth = Math.Max(TextureLabelWidth, ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f));
}
MissingMaterialConstants.Clear();
if( AssociatedShpk != null )
{
var setIdx = false;
foreach( var param in AssociatedShpk.MaterialParams.Where( m => !DefinedMaterialConstants.Contains( m.Id ) ) )
{
var (name, _) = MaterialParamRangeName( prefix, param.ByteOffset >> 2, param.ByteSize >> 2 );
var label = name == null
? $"(ID: 0x{param.Id:X8})"
: $"{name} (ID: 0x{param.Id:X8})";
if( NewConstantId == param.Id )
{
setIdx = true;
NewConstantIdx = MissingMaterialConstants.Count;
}
MissingMaterialConstants.Add( ( label, param.Id, param.ByteSize ) );
}
if( !setIdx && MissingMaterialConstants.Count > 0 )
{
NewConstantIdx = 0;
NewConstantId = MissingMaterialConstants[ 0 ].Id;
}
}
TextureLabelWidth = TextureLabelWidth / UiHelpers.Scale + 4;
}
public void UpdateSamplers()
public void UpdateConstants()
{
Samplers.Clear();
DefinedSamplers.Clear();
OrphanedSamplers = new IndexSet( Mtrl.Textures.Length, true );
foreach( var (sampler, idx) in Mtrl.ShaderPackage.Samplers.WithIndex() )
static List<T> FindOrAddGroup<T>(List<(string, List<T>)> groups, string name)
{
DefinedSamplers.Add( sampler.SamplerId );
if( !OrphanedSamplers.Remove( sampler.TextureIndex ) )
{
++AliasedSamplerCount;
}
foreach (var (groupName, group) in groups)
if (string.Equals(name, groupName, StringComparison.Ordinal))
return group;
var shpk = AssociatedShpk?.GetSamplerById( sampler.SamplerId );
var label = shpk.HasValue
? $"#{idx}: {shpk.Value.Name} (ID: 0x{sampler.SamplerId:X8})##{sampler.SamplerId}"
: $"#{idx} (ID: 0x{sampler.SamplerId:X8})##{sampler.SamplerId}";
var fileName = $"Texture #{sampler.TextureIndex} - {Path.GetFileName( Mtrl.Textures[ sampler.TextureIndex ].Path )}";
Samplers.Add( ( label, fileName, sampler.SamplerId ) );
var newGroup = new List<T>(16);
groups.Add((name, newGroup));
return newGroup;
}
MissingSamplers.Clear();
if( AssociatedShpk != null )
Constants.Clear();
if (AssociatedShpk == null)
{
var setSampler = false;
foreach( var sampler in AssociatedShpk.Samplers.Where( s => s.Slot == 2 && !DefinedSamplers.Contains( s.Id ) ) )
var fcGroup = FindOrAddGroup(Constants, "Further Constants");
foreach (var (constant, index) in Mtrl.ShaderPackage.Constants.WithIndex())
{
if( sampler.Id == NewSamplerId )
var values = Mtrl.GetConstantValues(constant);
for (var i = 0; i < values.Length; i += 4)
fcGroup.Add(($"0x{constant.Id:X8}", index, i..Math.Min(i + 4, values.Length), string.Empty, true, FloatConstantEditor.Default));
}
}
else
{
var prefix = AssociatedShpk.GetConstantById(MaterialParamsConstantId)?.Name ?? string.Empty;
foreach (var shpkConstant in AssociatedShpk.MaterialParams)
{
if ((shpkConstant.ByteSize & 0x3) != 0)
continue;
var constant = Mtrl.GetOrAddConstant(shpkConstant.Id, shpkConstant.ByteSize >> 2, out var constantIndex);
var values = Mtrl.GetConstantValues(constant);
var handledElements = new IndexSet(values.Length, false);
var dkData = TryGetShpkDevkitData<DevkitConstant[]>("Constants", shpkConstant.Id, true);
if (dkData != null)
{
setSampler = true;
NewSamplerIdx = MissingSamplers.Count;
foreach (var dkConstant in dkData)
{
var offset = (int)dkConstant.Offset;
var length = values.Length - offset;
if (dkConstant.Length.HasValue)
length = Math.Min(length, (int)dkConstant.Length.Value);
if (length <= 0)
continue;
var editor = dkConstant.CreateEditor();
if (editor != null)
FindOrAddGroup(Constants, dkConstant.Group.Length > 0 ? dkConstant.Group : "Further Constants")
.Add((dkConstant.Label, constantIndex, offset..(offset + length), dkConstant.Description, false, editor));
handledElements.AddRange(offset, length);
}
}
MissingSamplers.Add( ( sampler.Name, sampler.Id ) );
}
if( !setSampler && MissingSamplers.Count > 0 )
{
NewSamplerIdx = 0;
NewSamplerId = MissingSamplers[ 0 ].Id;
var fcGroup = FindOrAddGroup(Constants, "Further Constants");
foreach (var (start, end) in handledElements.Ranges(true))
{
if ((shpkConstant.ByteOffset & 0x3) == 0)
{
var offset = shpkConstant.ByteOffset >> 2;
for (int i = (start & ~0x3) - (offset & 0x3), j = offset >> 2; i < end; i += 4, ++j)
{
var rangeStart = Math.Max(i, start);
var rangeEnd = Math.Min(i + 4, end);
if (rangeEnd > rangeStart)
fcGroup.Add(($"{prefix}[{j:D2}]{VectorSwizzle((offset + rangeStart) & 0x3, (offset + rangeEnd - 1) & 0x3)} (0x{shpkConstant.Id:X8})", constantIndex, rangeStart..rangeEnd, string.Empty, true, FloatConstantEditor.Default));
}
}
else
{
for (var i = start; i < end; i += 4)
fcGroup.Add(($"0x{shpkConstant.Id:X8}", constantIndex, i..Math.Min(i + 4, end), string.Empty, true, FloatConstantEditor.Default));
}
}
}
}
Constants.RemoveAll(group => group.Constants.Count == 0);
Constants.Sort((x, y) =>
{
if (string.Equals(x.Header, "Further Constants", StringComparison.Ordinal))
return 1;
if (string.Equals(y.Header, "Further Constants", StringComparison.Ordinal))
return -1;
return string.Compare(x.Header, y.Header, StringComparison.Ordinal);
});
// HACK the Replace makes w appear after xyz, for the cbuffer-location-based naming scheme
foreach (var (_, group) in Constants)
group.Sort((x, y) => string.CompareOrdinal(
x.MonoFont ? x.Label.Replace("].w", "].{") : x.Label,
y.MonoFont ? y.Label.Replace("].w", "].{") : y.Label));
}
public unsafe void BindToMaterialInstances()
@ -329,6 +479,7 @@ public partial class ModEditWindow
// Carry on without that previewer.
}
}
UpdateMaterialPreview();
var colorSet = Mtrl.ColorSets.FirstOrNull(colorSet => colorSet.HasRows);
@ -378,6 +529,19 @@ public partial class ModEditWindow
previewer.SetSamplerFlags(samplerCrc, samplerFlags);
}
public void UpdateMaterialPreview()
{
SetShaderPackageFlags(Mtrl.ShaderPackage.Flags);
foreach (var constant in Mtrl.ShaderPackage.Constants)
{
var values = Mtrl.GetConstantValues(constant);
if (values != null)
SetMaterialParameter(constant.Id, 0, values);
}
foreach (var sampler in Mtrl.ShaderPackage.Samplers)
SetSamplerFlags(sampler.SamplerId, sampler.Flags);
}
public void HighlightColorSetRow(int rowIdx)
{
var oldRowIdx = HighlightedColorSetRow;
@ -402,7 +566,7 @@ public partial class ModEditWindow
UpdateColorSetRowPreview(rowIdx);
}
public unsafe void UpdateColorSetRowPreview(int rowIdx)
public void UpdateColorSetRowPreview(int rowIdx)
{
if (ColorSetPreviewers.Count == 0)
return;
@ -415,12 +579,12 @@ public partial class ModEditWindow
var maybeColorDyeSet = Mtrl.ColorDyeSets.FirstOrNull(colorDyeSet => colorDyeSet.Index == colorSet.Index);
var row = colorSet.Rows[rowIdx];
if (maybeColorDyeSet.HasValue)
if (maybeColorDyeSet.HasValue && UseColorDyeSet)
{
var stm = _edit._stainService.StmFile;
var dye = maybeColorDyeSet.Value.Rows[rowIdx];
if (stm.TryGetValue(dye.Template, (StainId)_edit._stainService.StainCombo.CurrentSelection.Key, out var dyes))
ApplyDye(ref row, dye, dyes);
row.ApplyDyeTemplate(dye, dyes);
}
if (HighlightedColorSetRow == rowIdx)
@ -428,13 +592,12 @@ public partial class ModEditWindow
foreach (var previewer in ColorSetPreviewers)
{
fixed (Half* pDest = previewer.ColorSet)
Buffer.MemoryCopy(&row, pDest + LiveColorSetPreviewer.TextureWidth * 4 * rowIdx, LiveColorSetPreviewer.TextureWidth * 4 * sizeof(Half), sizeof(MtrlFile.ColorSet.Row));
row.AsHalves().CopyTo(previewer.ColorSet.AsSpan().Slice(LiveColorSetPreviewer.TextureWidth * 4 * rowIdx, LiveColorSetPreviewer.TextureWidth * 4));
previewer.ScheduleUpdate();
}
}
public unsafe void UpdateColorSetPreview()
public void UpdateColorSetPreview()
{
if (ColorSetPreviewers.Count == 0)
return;
@ -447,7 +610,7 @@ public partial class ModEditWindow
var maybeColorDyeSet = Mtrl.ColorDyeSets.FirstOrNull(colorDyeSet => colorDyeSet.Index == colorSet.Index);
var rows = colorSet.Rows;
if (maybeColorDyeSet.HasValue)
if (maybeColorDyeSet.HasValue && UseColorDyeSet)
{
var stm = _edit._stainService.StmFile;
var stainId = (StainId)_edit._stainService.StainCombo.CurrentSelection.Key;
@ -457,7 +620,7 @@ public partial class ModEditWindow
ref var row = ref rows[i];
var dye = colorDyeSet.Rows[i];
if (stm.TryGetValue(dye.Template, stainId, out var dyes))
ApplyDye(ref row, dye, dyes);
row.ApplyDyeTemplate(dye, dyes);
}
}
@ -466,26 +629,11 @@ public partial class ModEditWindow
foreach (var previewer in ColorSetPreviewers)
{
fixed (Half* pDest = previewer.ColorSet)
Buffer.MemoryCopy(&rows, pDest, LiveColorSetPreviewer.TextureLength * sizeof(Half), sizeof(MtrlFile.ColorSet.RowArray));
rows.AsHalves().CopyTo(previewer.ColorSet);
previewer.ScheduleUpdate();
}
}
private static void ApplyDye(ref MtrlFile.ColorSet.Row row, MtrlFile.ColorDyeSet.Row dye, StmFile.DyePack dyes)
{
if (dye.Diffuse)
row.Diffuse = dyes.Diffuse;
if (dye.Specular)
row.Specular = dyes.Specular;
if (dye.SpecularStrength)
row.SpecularStrength = dyes.SpecularPower;
if (dye.Emissive)
row.Emissive = dyes.Emissive;
if (dye.Gloss)
row.GlossStrength = dyes.Gloss;
}
private static void ApplyHighlight(ref MtrlFile.ColorSet.Row row, int time)
{
var level = Math.Sin(time * Math.PI / 16) * 0.5 + 0.5;
@ -498,18 +646,19 @@ public partial class ModEditWindow
public void Update()
{
UpdateTextureLabels();
UpdateShaderKeyLabels();
UpdateConstantLabels();
UpdateSamplers();
UpdateShaders();
UpdateTextures();
UpdateConstants();
}
public MtrlTab( ModEditWindow edit, MtrlFile file, string filePath, bool writable )
{
_edit = edit;
Mtrl = file;
FilePath = filePath;
Writable = writable;
_edit = edit;
Mtrl = file;
FilePath = filePath;
Writable = writable;
UseColorDyeSet = file.ColorDyeSets.Length > 0;
AssociatedBaseDevkit = TryLoadShpkDevkit( "_base", out LoadedBaseDevkitPathName );
LoadShpk( FindAssociatedShpk( out _, out _ ) );
if (writable)
BindToMaterialInstances();
@ -532,9 +681,96 @@ public partial class ModEditWindow
}
public bool Valid
=> Mtrl.Valid;
=> ShadersKnown && Mtrl.Valid;
public byte[] Write()
=> Mtrl.Write();
{
var output = Mtrl.Clone();
output.GarbageCollect(AssociatedShpk, SamplerIds, UseColorDyeSet);
return output.Write();
}
private sealed class DevkitShaderKeyValue
{
public string Label = string.Empty;
public string Description = string.Empty;
}
private sealed class DevkitShaderKey
{
public string Label = string.Empty;
public string Description = string.Empty;
public Dictionary<uint, DevkitShaderKeyValue> Values = new();
}
private sealed class DevkitSampler
{
public string Label = string.Empty;
public string Description = string.Empty;
public string DefaultTexture = string.Empty;
}
private enum DevkitConstantType
{
Hidden = -1,
Float = 0,
Integer = 1,
Color = 2,
Enum = 3,
}
private sealed class DevkitConstantValue
{
public string Label = string.Empty;
public string Description = string.Empty;
public float Value = 0.0f;
}
private sealed class DevkitConstant
{
public uint Offset = 0;
public uint? Length = null;
public string Group = string.Empty;
public string Label = string.Empty;
public string Description = string.Empty;
public DevkitConstantType Type = DevkitConstantType.Float;
public float? Minimum = null;
public float? Maximum = null;
public float? Speed = null;
public float RelativeSpeed = 0.0f;
public float Factor = 1.0f;
public float Bias = 0.0f;
public byte Precision = 3;
public string Unit = string.Empty;
public bool SquaredRgb = false;
public bool Clamped = false;
public DevkitConstantValue[] Values = Array.Empty<DevkitConstantValue>();
public IConstantEditor? CreateEditor()
{
switch (Type)
{
case DevkitConstantType.Hidden:
return null;
case DevkitConstantType.Float:
return new FloatConstantEditor(Minimum, Maximum, Speed ?? 0.1f, RelativeSpeed, Factor, Bias, Precision, Unit);
case DevkitConstantType.Integer:
return new IntConstantEditor(ToInteger(Minimum), ToInteger(Maximum), Speed ?? 0.25f, RelativeSpeed, Factor, Bias, Unit);
case DevkitConstantType.Color:
return new ColorConstantEditor(SquaredRgb, Clamped);
case DevkitConstantType.Enum:
return new EnumConstantEditor(Array.ConvertAll(Values, value => (value.Label, value.Value, value.Description)));
default:
return FloatConstantEditor.Default;
}
}
private int? ToInteger(float? value)
=> value.HasValue ? (int)Math.Clamp(MathF.Round(value.Value), int.MinValue, int.MaxValue) : null;
}
}
}
}

View file

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
@ -7,6 +8,7 @@ using Dalamud.Interface;
using Dalamud.Interface.ImGuiFileDialog;
using ImGuiNET;
using Lumina.Data.Parsing;
using Lumina.Excel.GeneratedSheets;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData;
@ -19,20 +21,92 @@ public partial class ModEditWindow
{
private readonly FileDialogService _fileDialog;
private bool DrawPackageNameInput(MtrlTab tab, bool disabled)
// strings path/to/the.exe | grep --fixed-strings '.shpk' | sort -u | sed -e 's#^shader/sm5/shpk/##'
// Apricot shader packages are unlisted because
// 1. they cause performance/memory issues when calculating the effective shader set
// 2. they probably aren't intended for use with materials anyway
private static readonly IReadOnlyList<string> StandardShaderPackages = new string[]
{
var ret = false;
ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f);
if (ImGui.InputText("Shader Package Name", ref tab.Mtrl.ShaderPackage.Name, 63,
disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))
"3dui.shpk",
// "apricot_decal_dummy.shpk",
// "apricot_decal_ring.shpk",
// "apricot_decal.shpk",
// "apricot_lightmodel.shpk",
// "apricot_model_dummy.shpk",
// "apricot_model_morph.shpk",
// "apricot_model.shpk",
// "apricot_powder_dummy.shpk",
// "apricot_powder.shpk",
// "apricot_shape_dummy.shpk",
// "apricot_shape.shpk",
"bgcolorchange.shpk",
"bgcrestchange.shpk",
"bgdecal.shpk",
"bg.shpk",
"bguvscroll.shpk",
"channeling.shpk",
"characterglass.shpk",
"character.shpk",
"cloud.shpk",
"createviewposition.shpk",
"crystal.shpk",
"directionallighting.shpk",
"directionalshadow.shpk",
"grass.shpk",
"hair.shpk",
"iris.shpk",
"lightshaft.shpk",
"linelighting.shpk",
"planelighting.shpk",
"pointlighting.shpk",
"river.shpk",
"shadowmask.shpk",
"skin.shpk",
"spotlighting.shpk",
"verticalfog.shpk",
"water.shpk",
"weather.shpk",
};
private enum TextureAddressMode : uint
{
Wrap = 0,
Mirror = 1,
Clamp = 2,
Border = 3,
}
private static readonly IReadOnlyList<string> TextureAddressModeTooltips = new string[]
{
"Tile the texture at every UV integer junction.\n\nFor example, for U values between 0 and 3, the texture is repeated three times.",
"Flip the texture at every UV integer junction.\n\nFor U values between 0 and 1, for example, the texture is addressed normally; between 1 and 2, the texture is mirrored; between 2 and 3, the texture is normal again; and so on.",
"Texture coordinates outside the range [0.0, 1.0] are set to the texture color at 0.0 or 1.0, respectively.",
"Texture coordinates outside the range [0.0, 1.0] are set to the border color (generally black).",
};
private static bool DrawPackageNameInput(MtrlTab tab, bool disabled)
{
if (disabled)
{
ret = true;
tab.AssociatedShpk = null;
tab.LoadedShpkPath = FullPath.Empty;
ImGui.TextUnformatted("Shader Package: " + tab.Mtrl.ShaderPackage.Name);
return false;
}
if (ImGui.IsItemDeactivatedAfterEdit())
tab.LoadShpk(tab.FindAssociatedShpk(out _, out _));
var ret = false;
ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f);
using var c = ImRaii.Combo("Shader Package", tab.Mtrl.ShaderPackage.Name);
if (c)
foreach (var value in tab.GetShpkNames())
{
if (ImGui.Selectable(value, value == tab.Mtrl.ShaderPackage.Name))
{
tab.Mtrl.ShaderPackage.Name = value;
ret = true;
tab.AssociatedShpk = null;
tab.LoadedShpkPath = FullPath.Empty;
tab.LoadShpk(tab.FindAssociatedShpk(out _, out _));
}
}
return ret;
}
@ -41,8 +115,8 @@ public partial class ModEditWindow
{
var ret = false;
var shpkFlags = (int)tab.Mtrl.ShaderPackage.Flags;
ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f);
if (ImGui.InputInt("Shader Package Flags", ref shpkFlags, 0, 0,
ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f);
if (ImGui.InputInt("Shader Flags", ref shpkFlags, 0, 0,
ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)))
{
tab.Mtrl.ShaderPackage.Flags = (uint)shpkFlags;
@ -62,6 +136,12 @@ public partial class ModEditWindow
var text = tab.AssociatedShpk == null
? "Associated .shpk file: None"
: $"Associated .shpk file: {tab.LoadedShpkPathName}";
var devkitText = tab.AssociatedShpkDevkit == null
? "Associated dev-kit file: None"
: $"Associated dev-kit file: {tab.LoadedShpkDevkitPathName}";
var baseDevkitText = tab.AssociatedBaseDevkit == null
? "Base dev-kit file: None"
: $"Base dev-kit file: {tab.LoadedBaseDevkitPathName}";
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
@ -70,6 +150,16 @@ public partial class ModEditWindow
ImGuiUtil.HoverTooltip("Click to copy file path to clipboard.");
if (ImGui.Selectable(devkitText))
ImGui.SetClipboardText(tab.LoadedShpkDevkitPathName);
ImGuiUtil.HoverTooltip("Click to copy file path to clipboard.");
if (ImGui.Selectable(baseDevkitText))
ImGui.SetClipboardText(tab.LoadedBaseDevkitPathName);
ImGuiUtil.HoverTooltip("Click to copy file path to clipboard.");
if (ImGui.Button("Associate Custom .shpk File"))
_fileDialog.OpenFilePicker("Associate Custom .shpk File...", ".shpk", (success, name) =>
{
@ -94,94 +184,50 @@ public partial class ModEditWindow
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
}
private static bool DrawShaderKey(MtrlTab tab, bool disabled, ref int idx)
{
var ret = false;
using var t2 = ImRaii.TreeNode(tab.ShaderKeyLabels[idx], disabled ? ImGuiTreeNodeFlags.Leaf : 0);
if (!t2 || disabled)
return ret;
var key = tab.Mtrl.ShaderPackage.ShaderKeys[idx];
var shpkKey = tab.AssociatedShpk?.GetMaterialKeyById(key.Category);
if (shpkKey.HasValue)
{
ImGui.SetNextItemWidth(UiHelpers.Scale * 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))
{
tab.Mtrl.ShaderPackage.ShaderKeys[idx].Value = value;
ret = true;
tab.UpdateShaderKeyLabels();
}
}
}
if (ImGui.Button("Remove Key"))
{
tab.Mtrl.ShaderPackage.ShaderKeys = tab.Mtrl.ShaderPackage.ShaderKeys.RemoveItems(idx--);
ret = true;
tab.UpdateShaderKeyLabels();
}
return ret;
}
private static bool DrawNewShaderKey(MtrlTab tab)
{
ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f);
var ret = false;
using (var c = ImRaii.Combo("##NewConstantId", $"ID: 0x{tab.NewKeyId:X8}"))
{
if (c)
foreach (var idx in tab.MissingShaderKeyIndices)
{
var key = tab.AssociatedShpk!.MaterialKeys[idx];
if (ImGui.Selectable($"ID: 0x{key.Id:X8}", key.Id == tab.NewKeyId))
{
tab.NewKeyDefault = key.DefaultValue;
tab.NewKeyId = key.Id;
ret = true;
tab.UpdateShaderKeyLabels();
}
}
}
ImGui.SameLine();
if (ImGui.Button("Add Key"))
{
tab.Mtrl.ShaderPackage.ShaderKeys = tab.Mtrl.ShaderPackage.ShaderKeys.AddItem(new ShaderKey
{
Category = tab.NewKeyId,
Value = tab.NewKeyDefault,
});
ret = true;
tab.UpdateShaderKeyLabels();
}
return ret;
}
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)
if (tab.ShaderKeys.Count == 0)
return false;
var ret = false;
for (var idx = 0; idx < tab.Mtrl.ShaderPackage.ShaderKeys.Length; ++idx)
ret |= DrawShaderKey(tab, disabled, ref idx);
foreach (var (label, index, description, monoFont, values) in tab.ShaderKeys)
{
using var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont);
ref var key = ref tab.Mtrl.ShaderPackage.ShaderKeys[index];
var shpkKey = tab.AssociatedShpk?.GetMaterialKeyById(key.Category);
var currentValue = key.Value;
var (currentLabel, _, currentDescription) = values.FirstOrNull(v => v.Value == currentValue) ?? ($"0x{currentValue:X8}", currentValue, string.Empty);
if (!disabled && shpkKey.HasValue)
{
ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f);
using (var c = ImRaii.Combo($"##{key.Category:X8}", currentLabel))
{
if (c)
foreach (var (valueLabel, value, valueDescription) in values)
{
if (ImGui.Selectable(valueLabel, value == currentValue))
{
key.Value = value;
ret = true;
tab.Update();
}
if (!disabled && tab.AssociatedShpk != null && tab.MissingShaderKeyIndices.Count != 0)
ret |= DrawNewShaderKey(tab);
if (valueDescription.Length > 0)
ImGuiUtil.SelectableHelpMarker(valueDescription);
}
}
ImGui.SameLine();
if (description.Length > 0)
ImGuiUtil.LabeledHelpMarker(label, description);
else
ImGui.TextUnformatted(label);
}
else if (description.Length > 0 || currentDescription.Length > 0)
ImGuiUtil.LabeledHelpMarker($"{label}: {currentLabel}",
description + ((description.Length > 0 && currentDescription.Length > 0) ? "\n\n" : string.Empty) + currentDescription);
else
ImGui.TextUnformatted($"{label}: {currentLabel}");
}
return ret;
}
@ -191,162 +237,64 @@ public partial class ModEditWindow
if (tab.AssociatedShpk == null)
return;
ImRaii.TreeNode(tab.VertexShaders, ImGuiTreeNodeFlags.Leaf).Dispose();
ImRaii.TreeNode(tab.PixelShaders, ImGuiTreeNodeFlags.Leaf).Dispose();
}
ImRaii.TreeNode(tab.VertexShadersString, ImGuiTreeNodeFlags.Leaf).Dispose();
ImRaii.TreeNode(tab.PixelShadersString, ImGuiTreeNodeFlags.Leaf).Dispose();
private static bool DrawMaterialConstantValues(MtrlTab tab, bool disabled, ref int idx)
{
var (name, componentOnly, paramValueOffset) = tab.MaterialConstants[idx];
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
using var t2 = ImRaii.TreeNode(name);
if (!t2)
return false;
font.Dispose();
var constant = tab.Mtrl.ShaderPackage.Constants[idx];
var ret = false;
var values = tab.Mtrl.GetConstantValues(constant);
if (values.Length > 0)
if (tab.ShaderComment.Length > 0)
{
var valueOffset = constant.ByteOffset >> 2;
for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx)
{
var paramName = MaterialParamName(componentOnly, paramValueOffset + valueIdx) ?? $"#{valueIdx}";
ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f);
if (ImGui.InputFloat($"{paramName} (at 0x{(valueOffset + valueIdx) << 2:X4})", ref values[valueIdx], 0.0f, 0.0f, "%.3f",
disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))
{
ret = true;
tab.UpdateConstantLabels();
tab.SetMaterialParameter(constant.Id, valueIdx, values.Slice(valueIdx, 1));
}
}
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
ImGui.TextUnformatted(tab.ShaderComment);
}
else
{
ImRaii.TreeNode($"Offset: 0x{constant.ByteOffset:X4}", ImGuiTreeNodeFlags.Leaf).Dispose();
ImRaii.TreeNode($"Size: 0x{constant.ByteSize:X4}", ImGuiTreeNodeFlags.Leaf).Dispose();
}
if (!disabled
&& !tab.HasMalformedMaterialConstants
&& tab.OrphanedMaterialValues.Count == 0
&& tab.AliasedMaterialValueCount == 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 (tab.Mtrl.ShaderPackage.Constants[i].ByteOffset >= constant.ByteOffset)
tab.Mtrl.ShaderPackage.Constants[i].ByteOffset -= constant.ByteSize;
}
ret = true;
tab.UpdateConstantLabels();
tab.SetMaterialParameter(constant.Id, 0, new float[constant.ByteSize >> 2]);
}
return ret;
}
private static bool DrawMaterialOrphans(MtrlTab tab, bool disabled)
{
using var t2 = ImRaii.TreeNode($"Orphan Values ({tab.OrphanedMaterialValues.Count})");
if (!t2)
return false;
var ret = false;
foreach (var idx in tab.OrphanedMaterialValues)
{
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))
{
ret = true;
tab.UpdateConstantLabels();
}
}
return ret;
}
private static bool DrawNewMaterialParam(MtrlTab tab)
{
ImGui.SetNextItemWidth(UiHelpers.Scale * 450.0f);
using (var font = ImRaii.PushFont(UiBuilder.MonoFont))
{
using var c = ImRaii.Combo("##NewConstantId", tab.MissingMaterialConstants[tab.NewConstantIdx].Name);
if (c)
foreach (var (constant, idx) in tab.MissingMaterialConstants.WithIndex())
{
if (ImGui.Selectable(constant.Name, constant.Id == tab.NewConstantId))
{
tab.NewConstantIdx = idx;
tab.NewConstantId = constant.Id;
}
}
}
ImGui.SameLine();
if (ImGui.Button("Add Constant"))
{
var (_, _, byteSize) = tab.MissingMaterialConstants[tab.NewConstantIdx];
tab.Mtrl.ShaderPackage.Constants = tab.Mtrl.ShaderPackage.Constants.AddItem(new MtrlFile.Constant
{
Id = tab.NewConstantId,
ByteOffset = (ushort)(tab.Mtrl.ShaderPackage.ShaderValues.Length << 2),
ByteSize = byteSize,
});
tab.Mtrl.ShaderPackage.ShaderValues = tab.Mtrl.ShaderPackage.ShaderValues.AddItem(0.0f, byteSize >> 2);
tab.UpdateConstantLabels();
return true;
}
return false;
}
private static bool DrawMaterialConstants(MtrlTab tab, bool disabled)
{
if (tab.Mtrl.ShaderPackage.Constants.Length == 0
&& tab.Mtrl.ShaderPackage.ShaderValues.Length == 0
&& (disabled || tab.AssociatedShpk == null || tab.AssociatedShpk.MaterialParams.Length == 0))
if (tab.Constants.Count == 0)
return false;
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
using var t = ImRaii.TreeNode(tab.MaterialConstantLabel);
if (!t)
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
if (!ImGui.CollapsingHeader("Material Constants"))
return false;
font.Dispose();
using var _ = ImRaii.PushId("MaterialConstants");
var ret = false;
for (var idx = 0; idx < tab.Mtrl.ShaderPackage.Constants.Length; ++idx)
ret |= DrawMaterialConstantValues(tab, disabled, ref idx);
foreach (var (header, group) in tab.Constants)
{
using var t = ImRaii.TreeNode(header, ImGuiTreeNodeFlags.DefaultOpen);
if (!t)
continue;
if (tab.OrphanedMaterialValues.Count > 0)
ret |= DrawMaterialOrphans(tab, disabled);
else if (!disabled && !tab.HasMalformedMaterialConstants && tab.MissingMaterialConstants.Count > 0)
ret |= DrawNewMaterialParam(tab);
foreach (var (label, constantIndex, slice, description, monoFont, editor) in group)
{
var constant = tab.Mtrl.ShaderPackage.Constants[constantIndex];
var buffer = tab.Mtrl.GetConstantValues(constant);
if (buffer.Length > 0)
{
using var id = ImRaii.PushId($"##{constant.Id:X8}:{slice.Start}");
if (editor.Draw(buffer[slice], disabled, 250.0f))
{
ret = true;
tab.SetMaterialParameter(constant.Id, slice.Start, buffer[slice]);
}
ImGui.SameLine();
using var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont);
if (description.Length > 0)
ImGuiUtil.LabeledHelpMarker(label, description);
else
ImGui.TextUnformatted(label);
}
}
}
return ret;
}
private static bool DrawMaterialSampler(MtrlTab tab, bool disabled, ref int idx)
private static bool DrawMaterialSampler(MtrlTab tab, bool disabled, int textureIdx, int samplerIdx)
{
var (label, filename, samplerCrc) = tab.Samplers[idx];
using var tree = ImRaii.TreeNode(label);
if (!tree)
return false;
ImRaii.TreeNode(filename, ImGuiTreeNodeFlags.Leaf).Dispose();
var ret = false;
var sampler = tab.Mtrl.ShaderPackage.Samplers[idx];
var ret = false;
ref var texture = ref tab.Mtrl.Textures[textureIdx];
ref var sampler = ref tab.Mtrl.ShaderPackage.Samplers[samplerIdx];
// FIXME this probably doesn't belong here
static unsafe bool InputHexUInt16(string label, ref ushort v, ImGuiInputTextFlags flags)
@ -357,128 +305,123 @@ public partial class ModEditWindow
}
}
ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f);
if (InputHexUInt16("Texture Flags", ref tab.Mtrl.Textures[sampler.TextureIndex].Flags,
static bool ComboTextureAddressMode(string label, ref uint samplerFlags, int bitOffset)
{
var current = (TextureAddressMode)((samplerFlags >> bitOffset) & 0x3u);
using var c = ImRaii.Combo(label, current.ToString());
if (!c)
return false;
var ret = false;
foreach (var value in Enum.GetValues<TextureAddressMode>())
{
if (ImGui.Selectable(value.ToString(), value == current))
{
samplerFlags = (samplerFlags & ~(0x3u << bitOffset)) | ((uint)value << bitOffset);
ret = true;
}
ImGuiUtil.SelectableHelpMarker(TextureAddressModeTooltips[(int)value]);
}
return ret;
}
var dx11 = texture.DX11;
if (ImGui.Checkbox("Prepend -- to the file name on DirectX 11", ref dx11))
{
texture.DX11 = dx11;
ret = true;
}
ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f);
if (ComboTextureAddressMode("##UAddressMode", ref sampler.Flags, 2))
{
ret = true;
tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags);
}
ImGui.SameLine();
ImGuiUtil.LabeledHelpMarker("U Address Mode", "Method to use for resolving a U texture coordinate that is outside the 0 to 1 range.");
ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f);
if (ComboTextureAddressMode("##VAddressMode", ref sampler.Flags, 0))
{
ret = true;
tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags);
}
ImGui.SameLine();
ImGuiUtil.LabeledHelpMarker("V Address Mode", "Method to use for resolving a V texture coordinate that is outside the 0 to 1 range.");
var lodBias = ((int)(sampler.Flags << 12) >> 22) / 64.0f;
ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f);
if (ImGui.DragFloat("##LoDBias", ref lodBias, 0.1f, -8.0f, 7.984375f))
{
sampler.Flags = (uint)((sampler.Flags & ~0x000FFC00) | (uint)((int)Math.Round(Math.Clamp(lodBias, -8.0f, 7.984375f) * 64.0f) & 0x3FF) << 10);
ret = true;
tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags);
}
ImGui.SameLine();
ImGuiUtil.LabeledHelpMarker("Level of Detail Bias", "Offset from the calculated mipmap level.\n\nHigher means that the texture will start to lose detail nearer.\nLower means that the texture will keep its detail until farther.");
var minLod = (int)((sampler.Flags >> 20) & 0xF);
ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f);
if (ImGui.DragInt("##MinLoD", ref minLod, 0.1f, 0, 15))
{
sampler.Flags = (uint)((sampler.Flags & ~0x00F00000) | ((uint)Math.Clamp(minLod, 0, 15) << 20));
ret = true;
tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags);
}
ImGui.SameLine();
ImGuiUtil.LabeledHelpMarker("Minimum Level of Detail", "Most detailed mipmap level to use.\n\n0 is the full-sized texture, 1 is the half-sized texture, 2 is the quarter-sized texture, and so on.\n15 will forcibly reduce the texture to its smallest mipmap.");
using var t = ImRaii.TreeNode("Advanced Settings");
if (!t)
return ret;
ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f);
if (InputHexUInt16("Texture Flags", ref texture.Flags,
disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))
ret = true;
var samplerFlags = (int)sampler.Flags;
ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f);
ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f);
if (ImGui.InputInt("Sampler Flags", ref samplerFlags, 0, 0,
ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)))
{
tab.Mtrl.ShaderPackage.Samplers[idx].Flags = (uint)samplerFlags;
ret = true;
tab.SetSamplerFlags(samplerCrc, (uint)samplerFlags);
}
if (!disabled
&& tab.OrphanedSamplers.Count == 0
&& tab.AliasedSamplerCount == 0
&& ImGui.Button("Remove Sampler"))
{
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 (tab.Mtrl.ShaderPackage.Samplers[i].TextureIndex >= sampler.TextureIndex)
--tab.Mtrl.ShaderPackage.Samplers[i].TextureIndex;
}
ret = true;
tab.UpdateSamplers();
tab.UpdateTextureLabels();
sampler.Flags = (uint)samplerFlags;
ret = true;
tab.SetSamplerFlags(sampler.SamplerId, (uint)samplerFlags);
}
return ret;
}
private static bool DrawMaterialNewSampler(MtrlTab tab)
{
var (name, id) = tab.MissingSamplers[tab.NewSamplerIdx];
ImGui.SetNextItemWidth(UiHelpers.Scale * 450.0f);
using (var c = ImRaii.Combo("##NewSamplerId", $"{name} (ID: 0x{id:X8})"))
{
if (c)
foreach (var (sampler, idx) in tab.MissingSamplers.WithIndex())
{
if (ImGui.Selectable($"{sampler.Name} (ID: 0x{sampler.Id:X8})", sampler.Id == tab.NewSamplerId))
{
tab.NewSamplerIdx = idx;
tab.NewSamplerId = sampler.Id;
}
}
}
ImGui.SameLine();
if (!ImGui.Button("Add Sampler"))
return false;
var newSamplerId = tab.NewSamplerId;
tab.Mtrl.ShaderPackage.Samplers = tab.Mtrl.ShaderPackage.Samplers.AddItem(new Sampler
{
SamplerId = newSamplerId,
TextureIndex = (byte)tab.Mtrl.Textures.Length,
Flags = 0,
});
tab.Mtrl.Textures = tab.Mtrl.Textures.AddItem(new MtrlFile.Texture
{
Path = string.Empty,
Flags = 0,
});
tab.UpdateSamplers();
tab.UpdateTextureLabels();
tab.SetSamplerFlags(newSamplerId, 0);
return true;
}
private static bool DrawMaterialSamplers(MtrlTab tab, bool disabled)
{
if (tab.Mtrl.ShaderPackage.Samplers.Length == 0
&& tab.Mtrl.Textures.Length == 0
&& (disabled || (tab.AssociatedShpk?.Samplers.All(sampler => sampler.Slot != 2) ?? false)))
return false;
using var t = ImRaii.TreeNode("Samplers");
if (!t)
return false;
var ret = false;
for (var idx = 0; idx < tab.Mtrl.ShaderPackage.Samplers.Length; ++idx)
ret |= DrawMaterialSampler(tab, disabled, ref idx);
if (tab.OrphanedSamplers.Count > 0)
{
using var t2 = ImRaii.TreeNode($"Orphan Textures ({tab.OrphanedSamplers.Count})");
if (t2)
foreach (var idx in tab.OrphanedSamplers)
{
ImRaii.TreeNode($"#{idx}: {Path.GetFileName(tab.Mtrl.Textures[idx].Path)} - {tab.Mtrl.Textures[idx].Flags:X4}",
ImGuiTreeNodeFlags.Leaf)
.Dispose();
}
}
else if (!disabled && tab.MissingSamplers.Count > 0 && tab.AliasedSamplerCount == 0 && tab.Mtrl.Textures.Length < 255)
{
ret |= DrawMaterialNewSampler(tab);
}
return ret;
}
private bool DrawMaterialShaderResources(MtrlTab tab, bool disabled)
private bool DrawMaterialShader(MtrlTab tab, bool disabled)
{
var ret = false;
if (!ImGui.CollapsingHeader("Advanced Shader Resources"))
return ret;
if (ImGui.CollapsingHeader(tab.ShaderHeader))
{
ret |= DrawPackageNameInput(tab, disabled);
ret |= DrawShaderFlagsInput(tab, disabled);
DrawCustomAssociations(tab);
ret |= DrawMaterialShaderKeys(tab, disabled);
DrawMaterialShaders(tab);
}
if (tab.AssociatedShpkDevkit == null)
{
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
GC.KeepAlive(tab);
var textColor = ImGui.GetColorU32(ImGuiCol.Text);
var textColorWarning = (textColor & 0xFF000000u) | ((textColor & 0x00FEFEFE) >> 1) | (tab.AssociatedShpk == null ? 0x80u : 0x8080u); // Half red or yellow
using var c = ImRaii.PushColor(ImGuiCol.Text, textColorWarning);
ImGui.TextUnformatted(tab.AssociatedShpk == null
? "Unable to find a suitable .shpk file for cross-references. Some functionality will be missing."
: "No dev-kit file found for this material's shaders. Please install one for optimal editing experience, such as actual constant names instead of hexadecimal identifiers.");
}
ret |= DrawPackageNameInput(tab, disabled);
ret |= DrawShaderFlagsInput(tab, disabled);
DrawCustomAssociations(tab);
ret |= DrawMaterialShaderKeys(tab, disabled);
DrawMaterialShaders(tab);
ret |= DrawMaterialConstants(tab, disabled);
ret |= DrawMaterialSamplers(tab, disabled);
return ret;
}
@ -500,26 +443,25 @@ public partial class ModEditWindow
_ => null,
};
}
private static string VectorSwizzle(int firstComponent, int lastComponent)
=> (firstComponent, lastComponent) switch
{
(0, 4) => " ",
(0, 0) => ".x ",
(0, 1) => ".xy ",
(0, 2) => ".xyz ",
(0, 3) => " ",
(1, 1) => ".y ",
(1, 2) => ".yz ",
(1, 3) => ".yzw ",
(2, 2) => ".z ",
(2, 3) => ".zw ",
(3, 3) => ".w ",
_ => string.Empty,
};
private static (string? Name, bool ComponentOnly) MaterialParamRangeName(string prefix, int valueOffset, int valueLength)
{
static string VectorSwizzle(int firstComponent, int lastComponent)
=> (firstComponent, lastComponent) switch
{
(0, 4) => " ",
(0, 0) => ".x ",
(0, 1) => ".xy ",
(0, 2) => ".xyz ",
(0, 3) => " ",
(1, 1) => ".y ",
(1, 2) => ".yz ",
(1, 3) => ".yzw ",
(2, 2) => ".z ",
(2, 3) => ".zw ",
(3, 3) => ".w ",
_ => string.Empty,
};
if (valueLength == 0 || valueOffset < 0)
return (null, false);

View file

@ -18,16 +18,14 @@ public partial class ModEditWindow
DrawMaterialLivePreviewRebind( tab, disabled );
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
var ret = DrawMaterialTextureChange( tab, disabled );
var ret = DrawBackFaceAndTransparency( tab, disabled );
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
ret |= DrawBackFaceAndTransparency( tab, disabled );
ret |= DrawMaterialShader( tab, disabled );
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
ret |= DrawMaterialTextureChange( tab, disabled );
ret |= DrawMaterialColorSetChange( tab, disabled );
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
ret |= DrawMaterialShaderResources( tab, disabled );
ret |= DrawMaterialConstants( tab, disabled );
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
DrawOtherMaterialDetails( tab.Mtrl, disabled );
@ -40,35 +38,87 @@ public partial class ModEditWindow
if (disabled)
return;
if (ImGui.Button("Reload live-preview"))
if (ImGui.Button("Reload live preview"))
tab.BindToMaterialInstances();
if (tab.MaterialPreviewers.Count == 0 && tab.ColorSetPreviewers.Count == 0)
{
ImGui.SameLine();
var textColor = ImGui.GetColorU32(ImGuiCol.Text);
var textColorWarning = (textColor & 0xFF000000u) | ((textColor & 0x00FEFEFE) >> 1) | 0x80u; // Half red
using var c = ImRaii.PushColor(ImGuiCol.Text, textColorWarning);
ImGui.TextUnformatted("The current material has not been found on your character. Please check the Import from Screen tab for more information.");
}
}
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, tab.TextureLabelWidth * UiHelpers.Scale );
for( var i = 0; i < tab.Mtrl.Textures.Length; ++i )
if( tab.Textures.Count == 0 )
{
using var _ = ImRaii.PushId( i );
var tmp = tab.Mtrl.Textures[ i ].Path;
return false;
}
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
if( !ImGui.CollapsingHeader( "Textures and Samplers", ImGuiTreeNodeFlags.DefaultOpen ) )
{
return false;
}
var frameHeight = ImGui.GetFrameHeight();
var ret = false;
using var table = ImRaii.Table( "##Textures", 3 );
ImGui.TableSetupColumn( string.Empty, ImGuiTableColumnFlags.WidthFixed, frameHeight );
ImGui.TableSetupColumn( "Path" , ImGuiTableColumnFlags.WidthStretch );
ImGui.TableSetupColumn( "Name" , ImGuiTableColumnFlags.WidthFixed, tab.TextureLabelWidth * UiHelpers.Scale );
for( var i = 0; i < tab.Textures.Count; ++i )
{
var (label, textureI, samplerI, description, monoFont) = tab.Textures[i];
using var _ = ImRaii.PushId( samplerI );
var tmp = tab.Mtrl.Textures[ textureI ].Path;
var unfolded = tab.UnfoldedTextures.Contains( samplerI );
ImGui.TableNextColumn();
if( ImGuiUtil.DrawDisabledButton( ( unfolded ? FontAwesomeIcon.CaretDown : FontAwesomeIcon.CaretRight ).ToIconString(), new Vector2( frameHeight ),
"Settings for this texture and the associated sampler", false, true ) )
{
unfolded = !unfolded;
if( unfolded )
tab.UnfoldedTextures.Add( samplerI );
else
tab.UnfoldedTextures.Remove( samplerI );
}
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 != tab.Mtrl.Textures[ i ].Path )
&& tmp != tab.Mtrl.Textures[ textureI ].Path )
{
ret = true;
tab.Mtrl.Textures[ i ].Path = tmp;
ret = true;
tab.Mtrl.Textures[ textureI ].Path = tmp;
}
ImGui.TableNextColumn();
using var font = ImRaii.PushFont( UiBuilder.MonoFont );
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted( tab.TextureLabels[ i ] );
using( var font = ImRaii.PushFont( UiBuilder.MonoFont, monoFont ) )
{
ImGui.AlignTextToFramePadding();
if( description.Length > 0 )
ImGuiUtil.LabeledHelpMarker( label, description );
else
ImGui.TextUnformatted( label );
}
if( unfolded )
{
ImGui.TableNextColumn();
ImGui.TableNextColumn();
ret |= DrawMaterialSampler( tab, disabled, textureI, samplerI );
ImGui.TableNextColumn();
}
}
return ret;

View file

@ -14,7 +14,6 @@ using Penumbra.GameData;
using Penumbra.GameData.Data;
using Penumbra.GameData.Files;
using Penumbra.String;
using Penumbra.UI.AdvancedWindow;
using static Penumbra.GameData.Files.ShpkFile;
namespace Penumbra.UI.AdvancedWindow;
@ -40,7 +39,13 @@ public partial class ModEditWindow
ret |= DrawShaderPackageMaterialParamLayout( file, disabled );
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
ret |= DrawOtherShaderPackageDetails( file, disabled );
ret |= DrawShaderPackageResources( file, disabled );
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
DrawShaderPackageSelection( file );
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
DrawOtherShaderPackageDetails( file );
file.FileDialog.Draw();
@ -50,7 +55,18 @@ public partial class ModEditWindow
}
private static void DrawShaderPackageSummary( ShpkTab tab )
=> ImGui.TextUnformatted( tab.Header );
{
ImGui.TextUnformatted( tab.Header );
if( !tab.Shpk.Disassembled )
{
var textColor = ImGui.GetColorU32( ImGuiCol.Text );
var textColorWarning = ( textColor & 0xFF000000u ) | ( ( textColor & 0x00FEFEFE ) >> 1 ) | 0x80u; // Half red
using var c = ImRaii.PushColor( ImGuiCol.Text, textColorWarning );
ImGui.TextUnformatted( "Your system doesn't support disassembling shaders. Some functionality will be missing." );
}
}
private static void DrawShaderExportButton( ShpkTab tab, string objectName, Shader shader, int idx )
{
@ -163,7 +179,7 @@ public partial class ModEditWindow
}
DrawShaderExportButton( tab, objectName, shader, idx );
if( !disabled )
if( !disabled && tab.Shpk.Disassembled )
{
ImGui.SameLine();
DrawShaderImportButton( tab, objectName, shaders, idx );
@ -182,7 +198,8 @@ public partial class ModEditWindow
}
}
DrawRawDisassembly( shader );
if( tab.Shpk.Disassembled )
DrawRawDisassembly( shader );
}
return ret;
@ -276,7 +293,9 @@ public partial class ModEditWindow
private static bool DrawShaderPackageMaterialMatrix( ShpkTab tab, bool disabled )
{
ImGui.TextUnformatted( "Parameter positions (continuations are grayed out, unused values are red):" );
ImGui.TextUnformatted( tab.Shpk.Disassembled
? "Parameter positions (continuations are grayed out, unused values are red):"
: "Parameter positions (continuations are grayed out):" );
using var table = ImRaii.Table( "##MaterialParamLayout", 5,
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg );
@ -471,6 +490,22 @@ public partial class ModEditWindow
return ret;
}
private static bool DrawShaderPackageResources( ShpkTab tab, bool disabled )
{
var ret = false;
if( !ImGui.CollapsingHeader( "Shader Resources" ) )
{
return false;
}
ret |= DrawShaderPackageResourceArray( "Constant Buffers", "type", true, tab.Shpk.Constants, disabled );
ret |= DrawShaderPackageResourceArray( "Samplers", "type", false, tab.Shpk.Samplers, disabled );
ret |= DrawShaderPackageResourceArray( "Unordered Access Views", "type", false, tab.Shpk.Uavs, disabled );
return ret;
}
private static void DrawKeyArray( string arrayName, bool withId, IReadOnlyCollection< Key > keys )
{
if( keys.Count == 0 )
@ -513,7 +548,7 @@ public partial class ModEditWindow
foreach( var (node, idx) in tab.Shpk.Nodes.WithIndex() )
{
using var font = ImRaii.PushFont( UiBuilder.MonoFont );
using var t2 = ImRaii.TreeNode( $"#{idx:D4}: ID: 0x{node.Id:X8}" );
using var t2 = ImRaii.TreeNode( $"#{idx:D4}: Selector: 0x{node.Selector:X8}" );
if( !t2 )
{
continue;
@ -549,39 +584,38 @@ public partial class ModEditWindow
}
}
private static bool DrawOtherShaderPackageDetails( ShpkTab tab, bool disabled )
private static void DrawShaderPackageSelection( ShpkTab tab )
{
var ret = false;
if( !ImGui.CollapsingHeader( "Further Content" ) )
if( !ImGui.CollapsingHeader( "Shader Selection" ) )
{
return false;
return;
}
ImRaii.TreeNode( $"Version: 0x{tab.Shpk.Version:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose();
ret |= DrawShaderPackageResourceArray( "Constant Buffers", "type", true, tab.Shpk.Constants, disabled );
ret |= DrawShaderPackageResourceArray( "Samplers", "type", false, tab.Shpk.Samplers, disabled );
ret |= DrawShaderPackageResourceArray( "Unordered Access Views", "type", false, tab.Shpk.Uavs, disabled );
DrawKeyArray( "System Keys", true, tab.Shpk.SystemKeys );
DrawKeyArray( "Scene Keys", true, tab.Shpk.SceneKeys );
DrawKeyArray( "Material Keys", true, tab.Shpk.MaterialKeys );
DrawKeyArray( "Sub-View Keys", false, tab.Shpk.SubViewKeys );
DrawShaderPackageNodes( tab );
if( tab.Shpk.Items.Length > 0 )
using var t = ImRaii.TreeNode( $"Node Selectors ({tab.Shpk.NodeSelectors.Count})###NodeSelectors" );
if( t )
{
using var t = ImRaii.TreeNode( $"Items ({tab.Shpk.Items.Length})###Items" );
if( t )
using var font = ImRaii.PushFont( UiBuilder.MonoFont );
foreach( var selector in tab.Shpk.NodeSelectors )
{
using var font = ImRaii.PushFont( UiBuilder.MonoFont );
foreach( var (item, idx) in tab.Shpk.Items.WithIndex() )
{
ImRaii.TreeNode( $"#{idx:D4}: ID: 0x{item.Id:X8}, node: {item.Node}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose();
}
ImRaii.TreeNode( $"#{selector.Value:D4}: Selector: 0x{selector.Key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose();
}
}
}
private static void DrawOtherShaderPackageDetails( ShpkTab tab )
{
if( !ImGui.CollapsingHeader( "Further Content" ) )
{
return;
}
ImRaii.TreeNode( $"Version: 0x{tab.Shpk.Version:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose();
if( tab.Shpk.AdditionalData.Length > 0 )
{
@ -591,8 +625,6 @@ public partial class ModEditWindow
ImGuiUtil.TextWrapped( string.Join( ' ', tab.Shpk.AdditionalData.Select( c => $"{c:X2}" ) ) );
}
}
return ret;
}
private static string UsedComponentString( bool withSize, in Resource resource )

View file

@ -27,8 +27,16 @@ public partial class ModEditWindow
public ShpkTab(FileDialogService fileDialog, byte[] bytes)
{
FileDialog = fileDialog;
Shpk = new ShpkFile(bytes, true);
Header = $"Shader Package for DirectX {(int)Shpk.DirectXVersion}";
try
{
Shpk = new ShpkFile(bytes, true);
}
catch (NotImplementedException)
{
Shpk = new ShpkFile(bytes, false);
}
Header = $"Shader Package for DirectX {(int)Shpk.DirectXVersion}";
Extension = Shpk.DirectXVersion switch
{
ShpkFile.DxVersion.DirectX9 => ".cso",

View file

@ -1,4 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
@ -21,6 +23,7 @@ using Penumbra.Meta;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.String;
using Penumbra.String.Classes;
using Penumbra.UI.Classes;
using Penumbra.Util;
@ -523,6 +526,23 @@ public partial class ModEditWindow : Window, IDisposable
return new FullPath(path);
}
private HashSet<Utf8GamePath> FindPathsStartingWith(ByteString prefix)
{
var ret = new HashSet<Utf8GamePath>();
foreach (var path in _activeCollections.Current.ResolvedFiles.Keys)
if (path.Path.StartsWith(prefix))
ret.Add(path);
if (_mod != null)
foreach (var option in _mod.Groups.SelectMany(g => g).Append(_mod.Default))
foreach (var path in option.Files.Keys)
if (path.Path.StartsWith(prefix))
ret.Add(path);
return ret;
}
public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, IDataManager gameData,
Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager,
StainService stainService, ActiveCollections activeCollections, DalamudServices dalamud, ModMergeTab modMergeTab,

View file

@ -195,21 +195,7 @@ public class ModPanelSettingsTab : ITab
_collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, (uint)idx2);
if (option.Description.Length > 0)
{
var hovered = ImGui.IsItemHovered();
ImGui.SameLine();
using (var _ = ImRaii.PushFont(UiBuilder.IconFont))
{
using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled));
ImGuiUtil.RightAlign(FontAwesomeIcon.InfoCircle.ToIconString(), ImGui.GetStyle().ItemSpacing.X);
}
if (hovered)
{
using var tt = ImRaii.Tooltip();
ImGui.TextUnformatted(option.Description);
}
}
ImGuiUtil.SelectableHelpMarker(option.Description);
id.Pop();
}