DT material editor, main part

This commit is contained in:
Exter-N 2024-08-03 17:41:38 +02:00
parent 450751e43f
commit 36ab9573ae
21 changed files with 2744 additions and 2286 deletions

View file

@ -107,6 +107,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService
public bool AlwaysOpenDefaultImport { get; set; } = false;
public bool KeepDefaultMetaChanges { get; set; } = false;
public string DefaultModAuthor { get; set; } = DefaultTexToolsData.Author;
public bool EditRawTileTransforms { get; set; } = false;
public Dictionary<ColorId, uint> Colors { get; set; }
= Enum.GetValues<ColorId>().ToDictionary(c => c, c => c.Data().DefaultColor);

View file

@ -1,5 +1,6 @@
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Penumbra.GameData.Interop;
using Penumbra.Interop.SafeHandles;
@ -7,10 +8,6 @@ namespace Penumbra.Interop.MaterialPreview;
public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase
{
public const int TextureWidth = 4;
public const int TextureHeight = GameData.Files.MaterialStructs.LegacyColorTable.NumUsedRows;
public const int TextureLength = TextureWidth * TextureHeight * 4;
private readonly IFramework _framework;
private readonly Texture** _colorTableTexture;
@ -18,6 +15,9 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase
private bool _updatePending;
public int Width { get; }
public int Height { get; }
public Half[] ColorTable { get; }
public LiveColorTablePreviewer(ObjectManager objects, IFramework framework, MaterialInfo materialInfo)
@ -33,18 +33,24 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase
if (colorSetTextures == null)
throw new InvalidOperationException("Draw object doesn't have color table textures");
_colorTableTexture = colorSetTextures + (MaterialInfo.ModelSlot * 4 + MaterialInfo.MaterialSlot);
_colorTableTexture = colorSetTextures + (MaterialInfo.ModelSlot * CharacterBase.MaterialsPerSlot + MaterialInfo.MaterialSlot);
_originalColorTableTexture = new SafeTextureHandle(*_colorTableTexture, true);
if (_originalColorTableTexture == null)
throw new InvalidOperationException("Material doesn't have a color table");
ColorTable = new Half[TextureLength];
Width = (int)_originalColorTableTexture.Texture->Width;
Height = (int)_originalColorTableTexture.Texture->Height;
ColorTable = new Half[Width * Height * 4];
_updatePending = true;
framework.Update += OnFrameworkUpdate;
}
public Span<Half> GetColorRow(int i)
=> ColorTable.AsSpan().Slice(Width * 4 * i, Width * 4);
protected override void Clear(bool disposing, bool reset)
{
_framework.Update -= OnFrameworkUpdate;
@ -74,8 +80,8 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase
return;
var textureSize = stackalloc int[2];
textureSize[0] = TextureWidth;
textureSize[1] = TextureHeight;
textureSize[0] = Width;
textureSize[1] = Height;
using var texture =
new SafeTextureHandle(Device.Instance()->CreateTexture2D(textureSize, 1, 0x2460, 0x80000804, 7), false);
@ -104,6 +110,6 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase
if (colorSetTextures == null)
return false;
return _colorTableTexture == colorSetTextures + (MaterialInfo.ModelSlot * 4 + MaterialInfo.MaterialSlot);
return _colorTableTexture == colorSetTextures + (MaterialInfo.ModelSlot * CharacterBase.MaterialsPerSlot + MaterialInfo.MaterialSlot);
}
}

View file

@ -7,9 +7,9 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase
{
private readonly ShaderPackage* _shaderPackage;
private readonly uint _originalShPkFlags;
private readonly float[] _originalMaterialParameter;
private readonly uint[] _originalSamplerFlags;
private readonly uint _originalShPkFlags;
private readonly byte[] _originalMaterialParameter;
private readonly uint[] _originalSamplerFlags;
public LiveMaterialPreviewer(ObjectManager objects, MaterialInfo materialInfo)
: base(objects, materialInfo)
@ -28,7 +28,7 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase
_originalShPkFlags = Material->ShaderFlags;
_originalMaterialParameter = Material->MaterialParameterCBuffer->TryGetBuffer().ToArray();
_originalMaterialParameter = Material->MaterialParameterCBuffer->TryGetBuffer<byte>().ToArray();
_originalSamplerFlags = new uint[Material->TextureCount];
for (var i = 0; i < _originalSamplerFlags.Length; ++i)
@ -43,7 +43,7 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase
return;
Material->ShaderFlags = _originalShPkFlags;
var materialParameter = Material->MaterialParameterCBuffer->TryGetBuffer();
var materialParameter = Material->MaterialParameterCBuffer->TryGetBuffer<byte>();
if (!materialParameter.IsEmpty)
_originalMaterialParameter.AsSpan().CopyTo(materialParameter);
@ -59,7 +59,7 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase
Material->ShaderFlags = shPkFlags;
}
public void SetMaterialParameter(uint parameterCrc, Index offset, Span<float> value)
public void SetMaterialParameter(uint parameterCrc, Index offset, ReadOnlySpan<byte> value)
{
if (!CheckValidity())
return;
@ -68,7 +68,7 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase
if (constantBuffer == null)
return;
var buffer = constantBuffer->TryGetBuffer();
var buffer = constantBuffer->TryGetBuffer<byte>();
if (buffer.IsEmpty)
return;
@ -78,12 +78,10 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase
if (parameter.CRC != parameterCrc)
continue;
if ((parameter.Offset & 0x3) != 0
|| (parameter.Size & 0x3) != 0
|| (parameter.Offset + parameter.Size) >> 2 > buffer.Length)
if (parameter.Offset + parameter.Size > buffer.Length)
return;
value.TryCopyTo(buffer.Slice(parameter.Offset >> 2, parameter.Size >> 2)[offset..]);
value.TryCopyTo(buffer.Slice(parameter.Offset, parameter.Size)[offset..]);
return;
}
}

View file

@ -0,0 +1,509 @@
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using ImGuiNET;
using Penumbra.GameData.Files.MaterialStructs;
using Penumbra.GameData.Files;
using OtterGui.Text;
using Penumbra.GameData.Structs;
using OtterGui.Raii;
using OtterGui.Text.Widget;
namespace Penumbra.UI.AdvancedWindow.Materials;
public partial class MtrlTab
{
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 static readonly FontAwesomeCheckbox ApplyStainCheckbox = new(FontAwesomeIcon.FillDrip);
private static (Vector2 Scale, float Rotation, float Shear)? _pinnedTileTransform;
private bool DrawColorTableSection(bool disabled)
{
if ((!ShpkLoading && !SamplerIds.Contains(ShpkFile.TableSamplerId)) || Mtrl.Table == null)
return false;
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
if (!ImGui.CollapsingHeader("Color Table", ImGuiTreeNodeFlags.DefaultOpen))
return false;
ColorTableCopyAllClipboardButton();
ImGui.SameLine();
var ret = ColorTablePasteAllClipboardButton(disabled);
if (!disabled)
{
ImGui.SameLine();
ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0));
ImGui.SameLine();
ret |= ColorTableDyeableCheckbox();
}
if (Mtrl.DyeTable != null)
{
ImGui.SameLine();
ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0));
ImGui.SameLine();
ret |= DrawPreviewDye(disabled);
}
ret |= Mtrl.Table switch
{
LegacyColorTable legacyTable => DrawLegacyColorTable(legacyTable, Mtrl.DyeTable as LegacyColorDyeTable, disabled),
ColorTable table when Mtrl.ShaderPackage.Name is "characterlegacy.shpk" => DrawLegacyColorTable(table, Mtrl.DyeTable as ColorDyeTable, disabled),
_ => false,
};
return ret;
}
private void ColorTableCopyAllClipboardButton()
{
if (Mtrl.Table == null)
return;
if (!ImGui.Button("Export All Rows to Clipboard", ImGuiHelpers.ScaledVector2(200, 0)))
return;
try
{
var data1 = Mtrl.Table.AsBytes();
var data2 = Mtrl.DyeTable != null ? Mtrl.DyeTable.AsBytes() : [];
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 bool DrawPreviewDye(bool disabled)
{
var (dyeId1, (name1, dyeColor1, gloss1)) = _stainService.StainCombo1.CurrentSelection;
var (dyeId2, (name2, dyeColor2, gloss2)) = _stainService.StainCombo2.CurrentSelection;
var tt = dyeId1 == 0 && dyeId2 == 0
? "Select a preview dye first."u8
: "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled."u8;
if (ImUtf8.ButtonEx("Apply Preview Dye"u8, tt, disabled: disabled || dyeId1 == 0 && dyeId2 == 0))
{
var ret = false;
if (Mtrl.DyeTable != null)
{
ret |= Mtrl.ApplyDye(_stainService.LegacyStmFile, [dyeId1, dyeId2]);
ret |= Mtrl.ApplyDye(_stainService.GudStmFile, [dyeId1, dyeId2]);
}
UpdateColorTablePreview();
return ret;
}
ImGui.SameLine();
var label = dyeId1 == 0 ? "Preview Dye 1###previewDye1" : $"{name1} (Preview 1)###previewDye1";
if (_stainService.StainCombo1.Draw(label, dyeColor1, string.Empty, true, gloss1))
UpdateColorTablePreview();
ImGui.SameLine();
label = dyeId2 == 0 ? "Preview Dye 2###previewDye2" : $"{name2} (Preview 2)###previewDye2";
if (_stainService.StainCombo2.Draw(label, dyeColor2, string.Empty, true, gloss2))
UpdateColorTablePreview();
return false;
}
private bool ColorTablePasteAllClipboardButton(bool disabled)
{
if (Mtrl.Table == null)
return false;
if (!ImUtf8.ButtonEx("Import All Rows from Clipboard"u8, ImGuiHelpers.ScaledVector2(200, 0), disabled))
return false;
try
{
var text = ImGui.GetClipboardText();
var data = Convert.FromBase64String(text);
var table = Mtrl.Table.AsBytes();
var dyeTable = Mtrl.DyeTable != null ? Mtrl.DyeTable.AsBytes() : [];
if (data.Length != table.Length && data.Length != table.Length + dyeTable.Length)
return false;
data.AsSpan(0, table.Length).TryCopyTo(table);
data.AsSpan(table.Length).TryCopyTo(dyeTable);
UpdateColorTablePreview();
return true;
}
catch
{
return false;
}
}
[SkipLocalsInit]
private void ColorTableCopyClipboardButton(int rowIdx)
{
if (Mtrl.Table == null)
return;
if (!ImUtf8.IconButton(FontAwesomeIcon.Clipboard, "Export this row to your clipboard."u8,
ImGui.GetFrameHeight() * Vector2.One))
return;
try
{
var data1 = Mtrl.Table.RowAsBytes(rowIdx);
var data2 = Mtrl.DyeTable != null ? Mtrl.DyeTable.RowAsBytes(rowIdx) : [];
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 bool ColorTableDyeableCheckbox()
{
var dyeable = Mtrl.DyeTable != null;
var ret = ImGui.Checkbox("Dyeable", ref dyeable);
if (ret)
{
Mtrl.DyeTable = dyeable ? Mtrl.Table switch
{
ColorTable => new ColorDyeTable(),
LegacyColorTable => new LegacyColorDyeTable(),
_ => null,
} : null;
UpdateColorTablePreview();
}
return ret;
}
private bool ColorTablePasteFromClipboardButton(int rowIdx, bool disabled)
{
if (Mtrl.Table == null)
return false;
if (!ImUtf8.IconButton(FontAwesomeIcon.Paste, "Import an exported row from your clipboard onto this row."u8,
ImGui.GetFrameHeight() * Vector2.One, disabled))
return false;
try
{
var text = ImGui.GetClipboardText();
var data = Convert.FromBase64String(text);
var row = Mtrl.Table.RowAsBytes(rowIdx);
var dyeRow = Mtrl.DyeTable != null ? Mtrl.DyeTable.RowAsBytes(rowIdx) : [];
if (data.Length != row.Length && data.Length != row.Length + dyeRow.Length)
return false;
data.AsSpan(0, row.Length).TryCopyTo(row);
data.AsSpan(row.Length).TryCopyTo(dyeRow);
UpdateColorTableRowPreview(rowIdx);
return true;
}
catch
{
return false;
}
}
private void ColorTableHighlightButton(int pairIdx, bool disabled)
{
ImUtf8.IconButton(FontAwesomeIcon.Crosshairs, "Highlight this pair of rows on your character, if possible.\n\nHighlight colors can be configured in Penumbra's settings."u8,
ImGui.GetFrameHeight() * Vector2.One, disabled || ColorTablePreviewers.Count == 0);
if (ImGui.IsItemHovered())
HighlightColorTablePair(pairIdx);
else if (HighlightedColorTablePair == pairIdx)
CancelColorTableHighlight();
}
private static void CtBlendRect(Vector2 rcMin, Vector2 rcMax, uint topColor, uint bottomColor)
{
var style = ImGui.GetStyle();
var frameRounding = style.FrameRounding;
var frameThickness = style.FrameBorderSize;
var borderColor = ImGui.GetColorU32(ImGuiCol.Border);
var drawList = ImGui.GetWindowDrawList();
if (topColor == bottomColor)
drawList.AddRectFilled(rcMin, rcMax, topColor, frameRounding, ImDrawFlags.RoundCornersDefault);
else
{
drawList.AddRectFilled(
rcMin, rcMax with { Y = float.Lerp(rcMin.Y, rcMax.Y, 1.0f / 3) },
topColor, frameRounding, ImDrawFlags.RoundCornersTopLeft | ImDrawFlags.RoundCornersTopRight);
drawList.AddRectFilledMultiColor(
rcMin with { Y = float.Lerp(rcMin.Y, rcMax.Y, 1.0f / 3) },
rcMax with { Y = float.Lerp(rcMin.Y, rcMax.Y, 2.0f / 3) },
topColor, topColor, bottomColor, bottomColor);
drawList.AddRectFilled(
rcMin with { Y = float.Lerp(rcMin.Y, rcMax.Y, 2.0f / 3) }, rcMax,
bottomColor, frameRounding, ImDrawFlags.RoundCornersBottomLeft | ImDrawFlags.RoundCornersBottomRight);
}
drawList.AddRect(rcMin, rcMax, borderColor, frameRounding, ImDrawFlags.RoundCornersDefault, frameThickness);
}
private static bool CtColorPicker(ReadOnlySpan<byte> label, ReadOnlySpan<byte> description, HalfColor current, Action<HalfColor> setter, ReadOnlySpan<byte> letter = default)
{
var ret = false;
var inputSqrt = PseudoSqrtRgb((Vector3)current);
var tmp = inputSqrt;
if (ImUtf8.ColorEdit(label, ref tmp,
ImGuiColorEditFlags.NoInputs
| ImGuiColorEditFlags.DisplayRGB
| ImGuiColorEditFlags.InputRGB
| ImGuiColorEditFlags.NoTooltip
| ImGuiColorEditFlags.HDR)
&& tmp != inputSqrt)
{
setter((HalfColor)PseudoSquareRgb(tmp));
ret = true;
}
if (letter.Length > 0 && ImGui.IsItemVisible())
{
var textSize = ImUtf8.CalcTextSize(letter);
var center = ImGui.GetItemRectMin() + (ImGui.GetItemRectSize() - textSize) / 2;
var textColor = inputSqrt.LengthSquared() < 0.25f ? 0x80FFFFFFu : 0x80000000u;
ImGui.GetWindowDrawList().AddText(letter, center, textColor);
}
ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description);
return ret;
}
private static void CtColorPicker(ReadOnlySpan<byte> label, ReadOnlySpan<byte> description, HalfColor? current, ReadOnlySpan<byte> letter = default)
{
if (current.HasValue)
CtColorPicker(label, description, current.Value, Nop, letter);
else
{
var tmp = Vector4.Zero;
ImUtf8.ColorEdit(label, ref tmp,
ImGuiColorEditFlags.NoInputs
| ImGuiColorEditFlags.DisplayRGB
| ImGuiColorEditFlags.InputRGB
| ImGuiColorEditFlags.NoTooltip
| ImGuiColorEditFlags.HDR
| ImGuiColorEditFlags.AlphaPreview);
if (letter.Length > 0 && ImGui.IsItemVisible())
{
var textSize = ImUtf8.CalcTextSize(letter);
var center = ImGui.GetItemRectMin() + (ImGui.GetItemRectSize() - textSize) / 2;
ImGui.GetWindowDrawList().AddText(letter, center, 0x80000000u);
}
ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description);
}
}
private static bool CtApplyStainCheckbox(ReadOnlySpan<byte> label, ReadOnlySpan<byte> description, bool current, Action<bool> setter)
{
var tmp = current;
var result = ApplyStainCheckbox.Draw(label, ref tmp);
ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description);
if (!result || tmp == current)
return false;
setter(tmp);
return true;
}
private static bool CtDragHalf(ReadOnlySpan<byte> label, ReadOnlySpan<byte> description, Half value, ReadOnlySpan<byte> format, float min, float max, float speed, Action<Half> setter)
{
var tmp = (float)value;
var result = ImUtf8.DragScalar(label, ref tmp, format, min, max, speed);
ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description);
if (!result)
return false;
var newValue = (Half)tmp;
if (newValue == value)
return false;
setter(newValue);
return true;
}
private static bool CtDragHalf(ReadOnlySpan<byte> label, ReadOnlySpan<byte> description, ref Half value, ReadOnlySpan<byte> format, float min, float max, float speed)
{
var tmp = (float)value;
var result = ImUtf8.DragScalar(label, ref tmp, format, min, max, speed);
ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description);
if (!result)
return false;
var newValue = (Half)tmp;
if (newValue == value)
return false;
value = newValue;
return true;
}
private static void CtDragHalf(ReadOnlySpan<byte> label, ReadOnlySpan<byte> description, Half? value, ReadOnlySpan<byte> format)
{
using var _ = ImRaii.Disabled();
var valueOrDefault = value ?? Half.Zero;
var floatValue = (float)valueOrDefault;
CtDragHalf(label, description, valueOrDefault, value.HasValue ? format : "-"u8, floatValue, floatValue, 0.0f, Nop);
}
private static bool CtDragScalar<T>(ReadOnlySpan<byte> label, ReadOnlySpan<byte> description, T value, ReadOnlySpan<byte> format, T min, T max, float speed, Action<T> setter) where T : unmanaged, INumber<T>
{
var tmp = value;
var result = ImUtf8.DragScalar(label, ref tmp, format, min, max, speed);
ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description);
if (!result || tmp == value)
return false;
setter(tmp);
return true;
}
private static bool CtDragScalar<T>(ReadOnlySpan<byte> label, ReadOnlySpan<byte> description, ref T value, ReadOnlySpan<byte> format, T min, T max, float speed) where T : unmanaged, INumber<T>
{
var tmp = value;
var result = ImUtf8.DragScalar(label, ref tmp, format, min, max, speed);
ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description);
if (!result || tmp == value)
return false;
value = tmp;
return true;
}
private static void CtDragScalar<T>(ReadOnlySpan<byte> label, ReadOnlySpan<byte> description, T? value, ReadOnlySpan<byte> format) where T : unmanaged, INumber<T>
{
using var _ = ImRaii.Disabled();
var valueOrDefault = value ?? T.Zero;
CtDragScalar(label, description, valueOrDefault, value.HasValue ? format : "-"u8, valueOrDefault, valueOrDefault, 0.0f, Nop);
}
private bool CtTileIndexPicker(ReadOnlySpan<byte> label, ReadOnlySpan<byte> description, ushort value, bool compact, Action<ushort> setter)
{
if (!_materialTemplatePickers.DrawTileIndexPicker(label, description, ref value, compact))
return false;
setter(value);
return true;
}
private bool CtSphereMapIndexPicker(ReadOnlySpan<byte> label, ReadOnlySpan<byte> description, ushort value, bool compact, Action<ushort> setter)
{
if (!_materialTemplatePickers.DrawSphereMapIndexPicker(label, description, ref value, compact))
return false;
setter(value);
return true;
}
private bool CtTileTransformMatrix(HalfMatrix2x2 value, float floatSize, bool twoRowLayout, Action<HalfMatrix2x2> setter)
{
var ret = false;
if (_config.EditRawTileTransforms)
{
var tmp = value;
ImGui.SetNextItemWidth(floatSize);
ret |= CtDragHalf("##TileTransformUU"u8, "Tile Repeat U"u8, ref tmp.UU, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f);
ImUtf8.SameLineInner();
ImGui.SetNextItemWidth(floatSize);
ret |= CtDragHalf("##TileTransformVV"u8, "Tile Repeat V"u8, ref tmp.VV, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f);
if (!twoRowLayout)
ImUtf8.SameLineInner();
ImGui.SetNextItemWidth(floatSize);
ret |= CtDragHalf("##TileTransformUV"u8, "Tile Skew U"u8, ref tmp.UV, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f);
ImUtf8.SameLineInner();
ImGui.SetNextItemWidth(floatSize);
ret |= CtDragHalf("##TileTransformVU"u8, "Tile Skew V"u8, ref tmp.VU, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f);
if (!ret || tmp == value)
return false;
setter(tmp);
}
else
{
value.Decompose(out var scale, out var rotation, out var shear);
rotation *= 180.0f / MathF.PI;
shear *= 180.0f / MathF.PI;
ImGui.SetNextItemWidth(floatSize);
var scaleXChanged = CtDragScalar("##TileScaleU"u8, "Tile Scale U"u8, ref scale.X, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f);
var activated = ImGui.IsItemActivated();
var deactivated = ImGui.IsItemDeactivated();
ImUtf8.SameLineInner();
ImGui.SetNextItemWidth(floatSize);
var scaleYChanged = CtDragScalar("##TileScaleV"u8, "Tile Scale V"u8, ref scale.Y, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f);
activated |= ImGui.IsItemActivated();
deactivated |= ImGui.IsItemDeactivated();
if (!twoRowLayout)
ImUtf8.SameLineInner();
ImGui.SetNextItemWidth(floatSize);
var rotationChanged = CtDragScalar("##TileRotation"u8, "Tile Rotation"u8, ref rotation, "%.0f°"u8, -180.0f, 180.0f, 1.0f);
activated |= ImGui.IsItemActivated();
deactivated |= ImGui.IsItemDeactivated();
ImUtf8.SameLineInner();
ImGui.SetNextItemWidth(floatSize);
var shearChanged = CtDragScalar("##TileShear"u8, "Tile Shear"u8, ref shear, "%.0f°"u8, -90.0f, 90.0f, 1.0f);
activated |= ImGui.IsItemActivated();
deactivated |= ImGui.IsItemDeactivated();
if (deactivated)
_pinnedTileTransform = null;
else if (activated)
_pinnedTileTransform = (scale, rotation, shear);
ret = scaleXChanged | scaleYChanged | rotationChanged | shearChanged;
if (!ret)
return false;
if (_pinnedTileTransform.HasValue)
{
var (pinScale, pinRotation, pinShear) = _pinnedTileTransform.Value;
if (!scaleXChanged)
scale.X = pinScale.X;
if (!scaleYChanged)
scale.Y = pinScale.Y;
if (!rotationChanged)
rotation = pinRotation;
if (!shearChanged)
shear = pinShear;
}
var newValue = HalfMatrix2x2.Compose(scale, rotation * MathF.PI / 180.0f, shear * MathF.PI / 180.0f);
if (newValue == value)
return false;
setter(newValue);
}
return true;
}
/// <remarks> For use as setter of read-only fields. </remarks>
private static void Nop<T>(T _)
{ }
// Functions to deal with squared RGB values without making negatives useless.
internal static float PseudoSquareRgb(float x)
=> x < 0.0f ? -(x * x) : x * x;
internal static Vector3 PseudoSquareRgb(Vector3 vec)
=> new(PseudoSquareRgb(vec.X), PseudoSquareRgb(vec.Y), PseudoSquareRgb(vec.Z));
internal static Vector4 PseudoSquareRgb(Vector4 vec)
=> new(PseudoSquareRgb(vec.X), PseudoSquareRgb(vec.Y), PseudoSquareRgb(vec.Z), vec.W);
internal static float PseudoSqrtRgb(float x)
=> x < 0.0f ? -MathF.Sqrt(-x) : MathF.Sqrt(x);
internal static Vector3 PseudoSqrtRgb(Vector3 vec)
=> new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z));
internal static Vector4 PseudoSqrtRgb(Vector4 vec)
=> new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z), vec.W);
}

View file

@ -0,0 +1,277 @@
using Dalamud.Interface;
using ImGuiNET;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii;
using OtterGui.Text;
using OtterGui.Text.Widget.Editors;
using Penumbra.GameData.Files.ShaderStructs;
using static Penumbra.GameData.Files.ShpkFile;
namespace Penumbra.UI.AdvancedWindow.Materials;
public partial class MtrlTab
{
private const float MaterialConstantSize = 250.0f;
public readonly
List<(string Header, List<(string Label, int ConstantIndex, Range Slice, string Description, bool MonoFont, IEditor<byte> Editor)>
Constants)> Constants = new(16);
private void UpdateConstants()
{
static List<T> FindOrAddGroup<T>(List<(string, List<T>)> groups, string name)
{
foreach (var (groupName, group) in groups)
{
if (string.Equals(name, groupName, StringComparison.Ordinal))
return group;
}
var newGroup = new List<T>(16);
groups.Add((name, newGroup));
return newGroup;
}
Constants.Clear();
string mpPrefix;
if (AssociatedShpk == null)
{
mpPrefix = MaterialParamsConstantName.Value!;
var fcGroup = FindOrAddGroup(Constants, "Further Constants");
foreach (var (constant, index) in Mtrl.ShaderPackage.Constants.WithIndex())
{
var values = Mtrl.GetConstantValue<float>(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,
ConstantEditors.DefaultFloat));
}
}
}
else
{
mpPrefix = AssociatedShpk.GetConstantById(MaterialParamsConstantId)?.Name ?? MaterialParamsConstantName.Value!;
var autoNameMaxLength = Math.Max(Names.LongestKnownNameLength, mpPrefix.Length + 8);
foreach (var shpkConstant in AssociatedShpk.MaterialParams)
{
var name = Names.KnownNames.TryResolve(shpkConstant.Id);
var constant = Mtrl.GetOrAddConstant(shpkConstant.Id, AssociatedShpk, out var constantIndex);
var values = Mtrl.GetConstantValue<byte>(constant);
var handledElements = new IndexSet(values.Length, false);
var dkData = TryGetShpkDevkitData<DevkitConstant[]>("Constants", shpkConstant.Id, true);
if (dkData != null)
foreach (var dkConstant in dkData)
{
var offset = (int)dkConstant.EffectiveByteOffset;
var length = values.Length - offset;
var constantSize = dkConstant.EffectiveByteSize;
if (constantSize.HasValue)
length = Math.Min(length, (int)constantSize.Value);
if (length <= 0)
continue;
var editor = dkConstant.CreateEditor(_materialTemplatePickers);
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);
}
if (handledElements.IsFull)
continue;
var fcGroup = FindOrAddGroup(Constants, "Further Constants");
foreach (var (start, end) in handledElements.Ranges(complement: true))
{
if (start == 0 && end == values.Length && end - start <= 16)
{
if (name.Value != null)
{
fcGroup.Add((
$"{name.Value.PadRight(autoNameMaxLength)} (0x{shpkConstant.Id:X8})",
constantIndex, 0..values.Length, string.Empty, true, DefaultConstantEditorFor(name)));
continue;
}
}
if ((shpkConstant.ByteOffset & 0x3) == 0 && (shpkConstant.ByteSize & 0x3) == 0)
{
var offset = shpkConstant.ByteOffset;
for (int i = (start & ~0xF) - (offset & 0xF), j = offset >> 4; i < end; i += 16, ++j)
{
var rangeStart = Math.Max(i, start);
var rangeEnd = Math.Min(i + 16, end);
if (rangeEnd > rangeStart)
{
var autoName = $"{mpPrefix}[{j,2:D}]{VectorSwizzle(((offset + rangeStart) & 0xF) >> 2, ((offset + rangeEnd - 1) & 0xF) >> 2)}";
fcGroup.Add((
$"{autoName.PadRight(autoNameMaxLength)} (0x{shpkConstant.Id:X8})",
constantIndex, rangeStart..rangeEnd, string.Empty, true, DefaultConstantEditorFor(name)));
}
}
}
else
{
for (var i = start; i < end; i += 16)
{
fcGroup.Add(($"{"???".PadRight(autoNameMaxLength)} (0x{shpkConstant.Id:X8})", constantIndex, i..Math.Min(i + 16, end), string.Empty, true,
DefaultConstantEditorFor(name)));
}
}
}
}
}
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, and cbuffer-location names appear after known variable names
foreach (var (_, group) in Constants)
{
group.Sort((x, y) => string.CompareOrdinal(
x.MonoFont ? x.Label.Replace("].w", "].{").Replace(mpPrefix, "}_MaterialParameter") : x.Label,
y.MonoFont ? y.Label.Replace("].w", "].{").Replace(mpPrefix, "}_MaterialParameter") : y.Label));
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private IEditor<byte> DefaultConstantEditorFor(Name name)
=> ConstantEditors.DefaultFor(name, _materialTemplatePickers);
private bool DrawConstantsSection(bool disabled)
{
if (Constants.Count == 0)
return false;
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
if (!ImGui.CollapsingHeader("Material Constants"))
return false;
using var _ = ImRaii.PushId("MaterialConstants");
var ret = false;
foreach (var (header, group) in Constants)
{
using var t = ImRaii.TreeNode(header, ImGuiTreeNodeFlags.DefaultOpen);
if (!t)
continue;
foreach (var (label, constantIndex, slice, description, monoFont, editor) in group)
{
var constant = Mtrl.ShaderPackage.Constants[constantIndex];
var buffer = Mtrl.GetConstantValue<byte>(constant);
if (buffer.Length > 0)
{
using var id = ImRaii.PushId($"##{constant.Id:X8}:{slice.Start}");
ImGui.SetNextItemWidth(MaterialConstantSize * UiHelpers.Scale);
if (editor.Draw(buffer[slice], disabled))
{
ret = true;
SetMaterialParameter(constant.Id, slice.Start, buffer[slice]);
}
var shpkConstant = AssociatedShpk?.GetMaterialParamById(constant.Id);
var defaultConstantValue = shpkConstant.HasValue ? AssociatedShpk!.GetMaterialParamDefault<byte>(shpkConstant.Value) : [];
var defaultValue = IsValid(slice, defaultConstantValue.Length) ? defaultConstantValue[slice] : [];
var canReset = AssociatedShpk?.MaterialParamsDefaults != null
? defaultValue.Length > 0 && !defaultValue.SequenceEqual(buffer[slice])
: buffer[slice].ContainsAnyExcept((byte)0);
ImUtf8.SameLineInner();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Backspace.ToIconString(), ImGui.GetFrameHeight() * Vector2.One,
"Reset this constant to its default value.\n\nHold Ctrl to unlock.", !ImGui.GetIO().KeyCtrl || !canReset, true))
{
ret = true;
if (defaultValue.Length > 0)
defaultValue.CopyTo(buffer[slice]);
else
buffer[slice].Clear();
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 IsValid(Range range, int length)
{
var start = range.Start.GetOffset(length);
var end = range.End.GetOffset(length);
return start >= 0 && start <= length && end >= start && end <= length;
}
internal static string? MaterialParamName(bool componentOnly, int offset)
{
if (offset < 0)
return null;
return (componentOnly, offset & 0x3) switch
{
(true, 0) => "x",
(true, 1) => "y",
(true, 2) => "z",
(true, 3) => "w",
(false, 0) => $"[{offset >> 2:D2}].x",
(false, 1) => $"[{offset >> 2:D2}].y",
(false, 2) => $"[{offset >> 2:D2}].z",
(false, 3) => $"[{offset >> 2:D2}].w",
_ => null,
};
}
/// <remarks> Returned string is 4 chars long. </remarks>
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,
};
internal static (string? Name, bool ComponentOnly) MaterialParamRangeName(string prefix, int valueOffset, int valueLength)
{
if (valueLength == 0 || valueOffset < 0)
return (null, false);
var firstVector = valueOffset >> 2;
var lastVector = (valueOffset + valueLength - 1) >> 2;
var firstComponent = valueOffset & 0x3;
var lastComponent = (valueOffset + valueLength - 1) & 0x3;
if (firstVector == lastVector)
return ($"{prefix}[{firstVector}]{VectorSwizzle(firstComponent, lastComponent)}", true);
var sb = new StringBuilder(128);
sb.Append($"{prefix}[{firstVector}]{VectorSwizzle(firstComponent, 3).TrimEnd()}");
for (var i = firstVector + 1; i < lastVector; ++i)
sb.Append($", [{i}]");
sb.Append($", [{lastVector}]{VectorSwizzle(0, lastComponent)}");
return (sb.ToString(), false);
}
}

View file

@ -0,0 +1,240 @@
using Newtonsoft.Json.Linq;
using OtterGui.Text.Widget.Editors;
using Penumbra.String.Classes;
using static Penumbra.GameData.Files.ShpkFile;
namespace Penumbra.UI.AdvancedWindow.Materials;
public partial class MtrlTab
{
private JObject? TryLoadShpkDevkit(string shpkBaseName, out string devkitPathName)
{
try
{
if (!Utf8GamePath.FromString("penumbra/shpk_devkit/" + shpkBaseName + ".json", out var devkitPath))
throw new Exception("Could not assemble ShPk dev-kit path.");
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;
}
}
private T? TryGetShpkDevkitData<T>(string category, uint? id, bool mayVary) where T : class
=> TryGetShpkDevkitData<T>(AssociatedShpkDevkit, LoadedShpkDevkitPathName, category, id, mayVary)
?? TryGetShpkDevkitData<T>(AssociatedBaseDevkit, LoadedBaseDevkitPathName, category, id, mayVary);
private T? TryGetShpkDevkitData<T>(JObject? devkit, string devkitPathName, string category, uint? id, bool mayVary) where T : class
{
if (devkit == null)
return null;
try
{
var data = devkit[category];
if (id.HasValue)
data = data?[id.Value.ToString()];
if (mayVary && (data as JObject)?["Vary"] != null)
{
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];
}
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;
}
}
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 = [];
}
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,
/// <summary> Integer encoded as a float. </summary>
Integer = 1,
Color = 2,
Enum = 3,
/// <summary> Native integer. </summary>
Int32 = 4,
Int32Enum = 5,
Int8 = 6,
Int8Enum = 7,
Int16 = 8,
Int16Enum = 9,
Int64 = 10,
Int64Enum = 11,
Half = 12,
Double = 13,
TileIndex = 14,
SphereMapIndex = 15,
}
private sealed class DevkitConstantValue
{
public string Label = string.Empty;
public string Description = string.Empty;
public double Value = 0;
}
private sealed class DevkitConstant
{
public uint Offset = 0;
public uint? Length = null;
public uint? ByteOffset = null;
public uint? ByteSize = 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 Step = 0.0f;
public float StepFast = 0.0f;
public float? Speed = null;
public float RelativeSpeed = 0.0f;
public float Exponent = 1.0f;
public float Factor = 1.0f;
public float Bias = 0.0f;
public byte Precision = 3;
public bool Hex = false;
public bool Slider = true;
public bool Drag = true;
public string Unit = string.Empty;
public bool SquaredRgb = false;
public bool Clamped = false;
public DevkitConstantValue[] Values = [];
public uint EffectiveByteOffset
=> ByteOffset ?? Offset * ValueSize;
public uint? EffectiveByteSize
=> ByteSize ?? (Length * ValueSize);
public unsafe uint ValueSize
=> Type switch
{
DevkitConstantType.Hidden => sizeof(byte),
DevkitConstantType.Float => sizeof(float),
DevkitConstantType.Integer => sizeof(float),
DevkitConstantType.Color => sizeof(float),
DevkitConstantType.Enum => sizeof(float),
DevkitConstantType.Int32 => sizeof(int),
DevkitConstantType.Int32Enum => sizeof(int),
DevkitConstantType.Int8 => sizeof(byte),
DevkitConstantType.Int8Enum => sizeof(byte),
DevkitConstantType.Int16 => sizeof(short),
DevkitConstantType.Int16Enum => sizeof(short),
DevkitConstantType.Int64 => sizeof(long),
DevkitConstantType.Int64Enum => sizeof(long),
DevkitConstantType.Half => (uint)sizeof(Half),
DevkitConstantType.Double => sizeof(double),
DevkitConstantType.TileIndex => sizeof(float),
DevkitConstantType.SphereMapIndex => sizeof(float),
_ => sizeof(float),
};
public IEditor<byte>? CreateEditor(MaterialTemplatePickers? materialTemplatePickers)
=> Type switch
{
DevkitConstantType.Hidden => null,
DevkitConstantType.Float => CreateFloatEditor<float>().AsByteEditor(),
DevkitConstantType.Integer => CreateIntegerEditor<int>().IntAsFloatEditor().AsByteEditor(),
DevkitConstantType.Color => ColorEditor.Get(!Clamped).WithExponent(SquaredRgb ? 2.0f : 1.0f).AsByteEditor(),
DevkitConstantType.Enum => CreateEnumEditor(float.CreateSaturating).AsByteEditor(),
DevkitConstantType.Int32 => CreateIntegerEditor<int>().AsByteEditor(),
DevkitConstantType.Int32Enum => CreateEnumEditor(ToInteger<int>).AsByteEditor(),
DevkitConstantType.Int8 => CreateIntegerEditor<byte>(),
DevkitConstantType.Int8Enum => CreateEnumEditor(ToInteger<byte>),
DevkitConstantType.Int16 => CreateIntegerEditor<short>().AsByteEditor(),
DevkitConstantType.Int16Enum => CreateEnumEditor(ToInteger<short>).AsByteEditor(),
DevkitConstantType.Int64 => CreateIntegerEditor<long>().AsByteEditor(),
DevkitConstantType.Int64Enum => CreateEnumEditor(ToInteger<long>).AsByteEditor(),
DevkitConstantType.Half => CreateFloatEditor<Half>().AsByteEditor(),
DevkitConstantType.Double => CreateFloatEditor<double>().AsByteEditor(),
DevkitConstantType.TileIndex => materialTemplatePickers?.TileIndexPicker ?? ConstantEditors.DefaultIntAsFloat,
DevkitConstantType.SphereMapIndex => materialTemplatePickers?.SphereMapIndexPicker ?? ConstantEditors.DefaultIntAsFloat,
_ => ConstantEditors.DefaultFloat,
};
private IEditor<T> CreateIntegerEditor<T>()
where T : unmanaged, INumber<T>
=> ((Drag || Slider) && !Hex
? (Drag
? (IEditor<T>)DragEditor<T>.CreateInteger(ToInteger<T>(Minimum), ToInteger<T>(Maximum), Speed ?? 0.25f, RelativeSpeed, Unit, 0)
: SliderEditor<T>.CreateInteger(ToInteger<T>(Minimum) ?? default, ToInteger<T>(Maximum) ?? default, Unit, 0))
: InputEditor<T>.CreateInteger(ToInteger<T>(Minimum), ToInteger<T>(Maximum), ToInteger<T>(Step), ToInteger<T>(StepFast), Hex, Unit, 0))
.WithFactorAndBias(ToInteger<T>(Factor), ToInteger<T>(Bias));
private IEditor<T> CreateFloatEditor<T>()
where T : unmanaged, INumber<T>, IPowerFunctions<T>
=> ((Drag || Slider)
? (Drag
? (IEditor<T>)DragEditor<T>.CreateFloat(ToFloat<T>(Minimum), ToFloat<T>(Maximum), Speed ?? 0.1f, RelativeSpeed, Precision, Unit, 0)
: SliderEditor<T>.CreateFloat(ToFloat<T>(Minimum) ?? default, ToFloat<T>(Maximum) ?? default, Precision, Unit, 0))
: InputEditor<T>.CreateFloat(ToFloat<T>(Minimum), ToFloat<T>(Maximum), T.CreateSaturating(Step), T.CreateSaturating(StepFast), Precision, Unit, 0))
.WithExponent(T.CreateSaturating(Exponent))
.WithFactorAndBias(T.CreateSaturating(Factor), T.CreateSaturating(Bias));
private EnumEditor<T> CreateEnumEditor<T>(Func<double, T> convertValue)
where T : unmanaged, IUtf8SpanFormattable, IEqualityOperators<T, T, bool>
=> new(Array.ConvertAll(Values, value => (ToUtf8(value.Label), convertValue(value.Value), ToUtf8(value.Description))));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static T ToInteger<T>(float value) where T : struct, INumberBase<T>
=> T.CreateSaturating(MathF.Round(value));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static T ToInteger<T>(double value) where T : struct, INumberBase<T>
=> T.CreateSaturating(Math.Round(value));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static T? ToInteger<T>(float? value) where T : struct, INumberBase<T>
=> value.HasValue ? ToInteger<T>(value.Value) : null;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static T? ToFloat<T>(float? value) where T : struct, INumberBase<T>
=> value.HasValue ? T.CreateSaturating(value.Value) : null;
private static ReadOnlyMemory<byte> ToUtf8(string value)
=> Encoding.UTF8.GetBytes(value);
}
}

View file

@ -0,0 +1,368 @@
using Dalamud.Interface;
using Dalamud.Interface.Utility.Raii;
using ImGuiNET;
using OtterGui;
using OtterGui.Text;
using Penumbra.GameData.Files.MaterialStructs;
using Penumbra.GameData.Files.StainMapStructs;
using Penumbra.Services;
namespace Penumbra.UI.AdvancedWindow.Materials;
public partial class MtrlTab
{
private const float LegacyColorTableFloatSize = 65.0f;
private const float LegacyColorTablePercentageSize = 50.0f;
private const float LegacyColorTableIntegerSize = 40.0f;
private const float LegacyColorTableByteSize = 25.0f;
private bool DrawLegacyColorTable(LegacyColorTable table, LegacyColorDyeTable? dyeTable, bool disabled)
{
using var imTable = ImUtf8.Table("##ColorTable"u8, dyeTable != null ? 10 : 8,
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV);
if (!imTable)
return false;
DrawLegacyColorTableHeader(dyeTable != null);
var ret = false;
for (var i = 0; i < LegacyColorTable.NumRows; ++i)
{
if (DrawLegacyColorTableRow(table, dyeTable, i, disabled))
{
UpdateColorTableRowPreview(i);
ret = true;
}
ImGui.TableNextRow();
}
return ret;
}
private bool DrawLegacyColorTable(ColorTable table, ColorDyeTable? dyeTable, bool disabled)
{
using var imTable = ImUtf8.Table("##ColorTable"u8, dyeTable != null ? 10 : 8,
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV);
if (!imTable)
return false;
DrawLegacyColorTableHeader(dyeTable != null);
var ret = false;
for (var i = 0; i < ColorTable.NumRows; ++i)
{
if (DrawLegacyColorTableRow(table, dyeTable, i, disabled))
{
UpdateColorTableRowPreview(i);
ret = true;
}
ImGui.TableNextRow();
}
return ret;
}
private static void DrawLegacyColorTableHeader(bool hasDyeTable)
{
ImGui.TableNextColumn();
ImUtf8.TableHeader(default(ReadOnlySpan<byte>));
ImGui.TableNextColumn();
ImUtf8.TableHeader("Row"u8);
ImGui.TableNextColumn();
ImUtf8.TableHeader("Diffuse"u8);
ImGui.TableNextColumn();
ImUtf8.TableHeader("Specular"u8);
ImGui.TableNextColumn();
ImUtf8.TableHeader("Emissive"u8);
ImGui.TableNextColumn();
ImUtf8.TableHeader("Gloss"u8);
ImGui.TableNextColumn();
ImUtf8.TableHeader("Tile"u8);
ImGui.TableNextColumn();
ImUtf8.TableHeader("Repeat / Skew"u8);
if (hasDyeTable)
{
ImGui.TableNextColumn();
ImUtf8.TableHeader("Dye"u8);
ImGui.TableNextColumn();
ImUtf8.TableHeader("Dye Preview"u8);
}
}
private bool DrawLegacyColorTableRow(LegacyColorTable table, LegacyColorDyeTable? dyeTable, int rowIdx, bool disabled)
{
using var id = ImRaii.PushId(rowIdx);
ref var row = ref table[rowIdx];
var dye = dyeTable != null ? dyeTable[rowIdx] : default;
var floatSize = LegacyColorTableFloatSize * UiHelpers.Scale;
var pctSize = LegacyColorTablePercentageSize * UiHelpers.Scale;
var intSize = LegacyColorTableIntegerSize * UiHelpers.Scale;
ImGui.TableNextColumn();
ColorTableCopyClipboardButton(rowIdx);
ImUtf8.SameLineInner();
var ret = ColorTablePasteFromClipboardButton(rowIdx, disabled);
if ((rowIdx & 1) == 0)
{
ImUtf8.SameLineInner();
ColorTableHighlightButton(rowIdx >> 1, disabled);
}
ImGui.TableNextColumn();
using (var font = ImRaii.PushFont(UiBuilder.MonoFont))
ImUtf8.Text($"{(rowIdx >> 1) + 1,2:D}{"AB"[rowIdx & 1]}");
ImGui.TableNextColumn();
using var dis = ImRaii.Disabled(disabled);
ret |= CtColorPicker("##Diffuse"u8, "Diffuse Color"u8, row.DiffuseColor,
c => table[rowIdx].DiffuseColor = c);
if (dyeTable != null)
{
ImUtf8.SameLineInner();
ret |= CtApplyStainCheckbox("##dyeDiffuse"u8, "Apply Diffuse Color on Dye"u8, dye.DiffuseColor,
b => dyeTable[rowIdx].DiffuseColor = b);
}
ImGui.TableNextColumn();
ret |= CtColorPicker("##Specular"u8, "Specular Color"u8, row.SpecularColor,
c => table[rowIdx].SpecularColor = c);
if (dyeTable != null)
{
ImUtf8.SameLineInner();
ret |= CtApplyStainCheckbox("##dyeSpecular"u8, "Apply Specular Color on Dye"u8, dye.SpecularColor,
b => dyeTable[rowIdx].SpecularColor = b);
}
ImGui.SameLine();
ImGui.SetNextItemWidth(pctSize);
ret |= CtDragScalar("##SpecularMask"u8, "Specular Strength"u8, (float)row.SpecularMask * 100.0f, "%.0f%%"u8, 0f, HalfMaxValue * 100.0f, 1.0f,
v => table[rowIdx].SpecularMask = (Half)(v * 0.01f));
if (dyeTable != null)
{
ImUtf8.SameLineInner();
ret |= CtApplyStainCheckbox("##dyeSpecularMask"u8, "Apply Specular Strength on Dye"u8, dye.SpecularMask,
b => dyeTable[rowIdx].SpecularMask = b);
}
ImGui.TableNextColumn();
ret |= CtColorPicker("##Emissive"u8, "Emissive Color"u8, row.EmissiveColor,
c => table[rowIdx].EmissiveColor = c);
if (dyeTable != null)
{
ImUtf8.SameLineInner();
ret |= CtApplyStainCheckbox("##dyeEmissive"u8, "Apply Emissive Color on Dye"u8, dye.EmissiveColor,
b => dyeTable[rowIdx].EmissiveColor = b);
}
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(floatSize);
var glossStrengthMin = ImGui.GetIO().KeyCtrl ? 0.0f : HalfEpsilon;
ret |= CtDragHalf("##Shininess"u8, "Gloss Strength"u8, row.Shininess, "%.1f"u8, glossStrengthMin, HalfMaxValue, Math.Max(0.1f, (float)row.Shininess * 0.025f),
v => table[rowIdx].Shininess = v);
if (dyeTable != null)
{
ImUtf8.SameLineInner();
ret |= CtApplyStainCheckbox("##dyeShininess"u8, "Apply Gloss Strength on Dye"u8, dye.Shininess,
b => dyeTable[rowIdx].Shininess = b);
}
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(intSize);
ret |= CtTileIndexPicker("##TileIndex"u8, "Tile Index"u8, row.TileIndex, true,
value => table[rowIdx].TileIndex = value);
ImGui.TableNextColumn();
ret |= CtTileTransformMatrix(row.TileTransform, floatSize, false,
m => table[rowIdx].TileTransform = m);
if (dyeTable != null)
{
ImGui.TableNextColumn();
if (_stainService.LegacyTemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, intSize
+ ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton))
{
dyeTable[rowIdx].Template = _stainService.LegacyTemplateCombo.CurrentSelection;
ret = true;
}
ImGuiUtil.HoverTooltip("Dye Template", ImGuiHoveredFlags.AllowWhenDisabled);
ImGui.TableNextColumn();
ret |= DrawLegacyDyePreview(rowIdx, disabled, dye, floatSize);
}
return ret;
}
private bool DrawLegacyColorTableRow(ColorTable table, ColorDyeTable? dyeTable, int rowIdx, bool disabled)
{
using var id = ImRaii.PushId(rowIdx);
ref var row = ref table[rowIdx];
var dye = dyeTable != null ? dyeTable[rowIdx] : default;
var floatSize = LegacyColorTableFloatSize * UiHelpers.Scale;
var pctSize = LegacyColorTablePercentageSize * UiHelpers.Scale;
var intSize = LegacyColorTableIntegerSize * UiHelpers.Scale;
var byteSize = LegacyColorTableByteSize * UiHelpers.Scale;
ImGui.TableNextColumn();
ColorTableCopyClipboardButton(rowIdx);
ImUtf8.SameLineInner();
var ret = ColorTablePasteFromClipboardButton(rowIdx, disabled);
if ((rowIdx & 1) == 0)
{
ImUtf8.SameLineInner();
ColorTableHighlightButton(rowIdx >> 1, disabled);
}
ImGui.TableNextColumn();
using (var font = ImRaii.PushFont(UiBuilder.MonoFont))
ImUtf8.Text($"{(rowIdx >> 1) + 1,2:D}{"AB"[rowIdx & 1]}");
ImGui.TableNextColumn();
using var dis = ImRaii.Disabled(disabled);
ret |= CtColorPicker("##Diffuse"u8, "Diffuse Color"u8, row.DiffuseColor,
c => table[rowIdx].DiffuseColor = c);
if (dyeTable != null)
{
ImUtf8.SameLineInner();
ret |= CtApplyStainCheckbox("##dyeDiffuse"u8, "Apply Diffuse Color on Dye"u8, dye.DiffuseColor,
b => dyeTable[rowIdx].DiffuseColor = b);
}
ImGui.TableNextColumn();
ret |= CtColorPicker("##Specular"u8, "Specular Color"u8, row.SpecularColor,
c => table[rowIdx].SpecularColor = c);
if (dyeTable != null)
{
ImUtf8.SameLineInner();
ret |= CtApplyStainCheckbox("##dyeSpecular"u8, "Apply Specular Color on Dye"u8, dye.SpecularColor,
b => dyeTable[rowIdx].SpecularColor = b);
}
ImGui.SameLine();
ImGui.SetNextItemWidth(pctSize);
ret |= CtDragScalar("##SpecularMask"u8, "Specular Strength"u8, (float)row.Scalar7 * 100.0f, "%.0f%%"u8, 0f, HalfMaxValue * 100.0f, 1.0f,
v => table[rowIdx].Scalar7 = (Half)(v * 0.01f));
if (dyeTable != null)
{
ImUtf8.SameLineInner();
ret |= CtApplyStainCheckbox("##dyeSpecularMask"u8, "Apply Specular Strength on Dye"u8, dye.Metalness,
b => dyeTable[rowIdx].Metalness = b);
}
ImGui.TableNextColumn();
ret |= CtColorPicker("##Emissive"u8, "Emissive Color"u8, row.EmissiveColor,
c => table[rowIdx].EmissiveColor = c);
if (dyeTable != null)
{
ImUtf8.SameLineInner();
ret |= CtApplyStainCheckbox("##dyeEmissive"u8, "Apply Emissive Color on Dye"u8, dye.EmissiveColor,
b => dyeTable[rowIdx].EmissiveColor = b);
}
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(floatSize);
var glossStrengthMin = ImGui.GetIO().KeyCtrl ? 0.0f : HalfEpsilon;
ret |= CtDragHalf("##Shininess"u8, "Gloss Strength"u8, row.Scalar3, "%.1f"u8, glossStrengthMin, HalfMaxValue, Math.Max(0.1f, (float)row.Scalar3 * 0.025f),
v => table[rowIdx].Scalar3 = v);
if (dyeTable != null)
{
ImUtf8.SameLineInner();
ret |= CtApplyStainCheckbox("##dyeShininess"u8, "Apply Gloss Strength on Dye"u8, dye.Scalar3,
b => dyeTable[rowIdx].Scalar3 = b);
}
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(intSize);
ret |= CtTileIndexPicker("##TileIndex"u8, "Tile Index"u8, row.TileIndex, true,
value => table[rowIdx].TileIndex = value);
ImUtf8.SameLineInner();
ImGui.SetNextItemWidth(pctSize);
ret |= CtDragScalar("##TileAlpha"u8, "Tile Opacity"u8, (float)row.TileAlpha * 100.0f, "%.0f%%"u8, 0f, HalfMaxValue * 100.0f, 1.0f,
v => table[rowIdx].TileAlpha = (Half)(v * 0.01f));
ImGui.TableNextColumn();
ret |= CtTileTransformMatrix(row.TileTransform, floatSize, false,
m => table[rowIdx].TileTransform = m);
if (dyeTable != null)
{
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(byteSize);
ret |= CtDragScalar("##DyeChannel"u8, "Dye Channel"u8, dye.Channel + 1, "%hhd"u8, 1, StainService.ChannelCount, 0.25f,
value => dyeTable[rowIdx].Channel = (byte)(Math.Clamp(value, 1, StainService.ChannelCount) - 1));
ImUtf8.SameLineInner();
_stainService.LegacyTemplateCombo.CurrentDyeChannel = dye.Channel;
if (_stainService.LegacyTemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, intSize
+ ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton))
{
dyeTable[rowIdx].Template = _stainService.LegacyTemplateCombo.CurrentSelection;
ret = true;
}
ImGuiUtil.HoverTooltip("Dye Template", ImGuiHoveredFlags.AllowWhenDisabled);
ImGui.TableNextColumn();
ret |= DrawLegacyDyePreview(rowIdx, disabled, dye, floatSize);
}
return ret;
}
private bool DrawLegacyDyePreview(int rowIdx, bool disabled, LegacyColorDyeTable.Row dye, float floatSize)
{
var stain = _stainService.StainCombo1.CurrentSelection.Key;
if (stain == 0 || !_stainService.LegacyStmFile.TryGetValue(dye.Template, stain, out var values))
return false;
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 && Mtrl.ApplyDyeToRow(_stainService.LegacyStmFile, [stain], rowIdx);
ImGui.SameLine();
DrawLegacyDyePreview(values, floatSize);
return ret;
}
private bool DrawLegacyDyePreview(int rowIdx, bool disabled, ColorDyeTable.Row dye, float floatSize)
{
var stain = _stainService.GetStainCombo(dye.Channel).CurrentSelection.Key;
if (stain == 0 || !_stainService.LegacyStmFile.TryGetValue(dye.Template, stain, out var values))
return false;
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 && Mtrl.ApplyDyeToRow(_stainService.LegacyStmFile, [
_stainService.StainCombo1.CurrentSelection.Key,
_stainService.StainCombo2.CurrentSelection.Key,
], rowIdx);
ImGui.SameLine();
DrawLegacyDyePreview(values, floatSize);
return ret;
}
private static void DrawLegacyDyePreview(LegacyDyePack values, float floatSize)
{
CtColorPicker("##diffusePreview"u8, default, values.DiffuseColor, "D"u8);
ImUtf8.SameLineInner();
CtColorPicker("##specularPreview"u8, default, values.SpecularColor, "S"u8);
ImUtf8.SameLineInner();
CtColorPicker("##emissivePreview"u8, default, values.EmissiveColor, "E"u8);
ImUtf8.SameLineInner();
using var dis = ImRaii.Disabled();
ImGui.SetNextItemWidth(floatSize);
var shininess = (float)values.Shininess;
ImGui.DragFloat("##shininessPreview", ref shininess, 0, shininess, shininess, "%.1f G");
ImUtf8.SameLineInner();
ImGui.SetNextItemWidth(floatSize);
var specularMask = (float)values.SpecularMask * 100.0f;
ImGui.DragFloat("##specularMaskPreview", ref specularMask, 0, specularMask, specularMask, "%.0f%% S");
}
}

View file

@ -0,0 +1,272 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using ImGuiNET;
using OtterGui.Raii;
using OtterGui.Text;
using Penumbra.GameData.Files.MaterialStructs;
using Penumbra.GameData.Structs;
using Penumbra.Interop.MaterialPreview;
using Penumbra.Services;
using Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow.Materials;
public partial class MtrlTab
{
public readonly List<LiveMaterialPreviewer> MaterialPreviewers = new(4);
public readonly List<LiveColorTablePreviewer> ColorTablePreviewers = new(4);
public int HighlightedColorTablePair = -1;
public readonly Stopwatch HighlightTime = new();
private void DrawMaterialLivePreviewRebind(bool disabled)
{
if (disabled)
return;
if (ImGui.Button("Reload live preview"))
BindToMaterialInstances();
if (MaterialPreviewers.Count != 0 || ColorTablePreviewers.Count != 0)
return;
ImGui.SameLine();
using var c = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder);
ImUtf8.Text(
"The current material has not been found on your character. Please check the Import from Screen tab for more information."u8);
}
public unsafe void BindToMaterialInstances()
{
UnbindFromMaterialInstances();
var instances = MaterialInfo.FindMaterials(_resourceTreeFactory.GetLocalPlayerRelatedCharacters().Select(ch => ch.Address),
FilePath);
var foundMaterials = new HashSet<nint>();
foreach (var materialInfo in instances)
{
var material = materialInfo.GetDrawObjectMaterial(_objects);
if (foundMaterials.Contains((nint)material))
continue;
try
{
MaterialPreviewers.Add(new LiveMaterialPreviewer(_objects, materialInfo));
foundMaterials.Add((nint)material);
}
catch (InvalidOperationException)
{
// Carry on without that previewer.
}
}
UpdateMaterialPreview();
if (Mtrl.Table == null)
return;
foreach (var materialInfo in instances)
{
try
{
ColorTablePreviewers.Add(new LiveColorTablePreviewer(_objects, _framework, materialInfo));
}
catch (InvalidOperationException)
{
// Carry on without that previewer.
}
}
UpdateColorTablePreview();
}
private void UnbindFromMaterialInstances()
{
foreach (var previewer in MaterialPreviewers)
previewer.Dispose();
MaterialPreviewers.Clear();
foreach (var previewer in ColorTablePreviewers)
previewer.Dispose();
ColorTablePreviewers.Clear();
}
private unsafe void UnbindFromDrawObjectMaterialInstances(CharacterBase* characterBase)
{
for (var i = MaterialPreviewers.Count; i-- > 0;)
{
var previewer = MaterialPreviewers[i];
if (previewer.DrawObject != characterBase)
continue;
previewer.Dispose();
MaterialPreviewers.RemoveAt(i);
}
for (var i = ColorTablePreviewers.Count; i-- > 0;)
{
var previewer = ColorTablePreviewers[i];
if (previewer.DrawObject != characterBase)
continue;
previewer.Dispose();
ColorTablePreviewers.RemoveAt(i);
}
}
public void SetShaderPackageFlags(uint shPkFlags)
{
foreach (var previewer in MaterialPreviewers)
previewer.SetShaderPackageFlags(shPkFlags);
}
public void SetMaterialParameter(uint parameterCrc, Index offset, Span<byte> value)
{
foreach (var previewer in MaterialPreviewers)
previewer.SetMaterialParameter(parameterCrc, offset, value);
}
public void SetSamplerFlags(uint samplerCrc, uint samplerFlags)
{
foreach (var previewer in MaterialPreviewers)
previewer.SetSamplerFlags(samplerCrc, samplerFlags);
}
private void UpdateMaterialPreview()
{
SetShaderPackageFlags(Mtrl.ShaderPackage.Flags);
foreach (var constant in Mtrl.ShaderPackage.Constants)
{
var values = Mtrl.GetConstantValue<byte>(constant);
if (values != null)
SetMaterialParameter(constant.Id, 0, values);
}
foreach (var sampler in Mtrl.ShaderPackage.Samplers)
SetSamplerFlags(sampler.SamplerId, sampler.Flags);
}
public void HighlightColorTablePair(int pairIdx)
{
var oldPairIdx = HighlightedColorTablePair;
if (HighlightedColorTablePair != pairIdx)
{
HighlightedColorTablePair = pairIdx;
HighlightTime.Restart();
}
if (oldPairIdx >= 0)
{
UpdateColorTableRowPreview(oldPairIdx << 1);
UpdateColorTableRowPreview((oldPairIdx << 1) | 1);
}
if (pairIdx >= 0)
{
UpdateColorTableRowPreview(pairIdx << 1);
UpdateColorTableRowPreview((pairIdx << 1) | 1);
}
}
public void CancelColorTableHighlight()
{
var pairIdx = HighlightedColorTablePair;
HighlightedColorTablePair = -1;
HighlightTime.Reset();
if (pairIdx >= 0)
{
UpdateColorTableRowPreview(pairIdx << 1);
UpdateColorTableRowPreview((pairIdx << 1) | 1);
}
}
public void UpdateColorTableRowPreview(int rowIdx)
{
if (ColorTablePreviewers.Count == 0)
return;
if (Mtrl.Table == null)
return;
var row = Mtrl.Table switch
{
LegacyColorTable legacyTable => new ColorTable.Row(legacyTable[rowIdx]),
ColorTable table => table[rowIdx],
_ => throw new InvalidOperationException($"Unsupported color table type {Mtrl.Table.GetType()}"),
};
if (Mtrl.DyeTable != null)
{
var dyeRow = Mtrl.DyeTable switch
{
LegacyColorDyeTable legacyDyeTable => new ColorDyeTable.Row(legacyDyeTable[rowIdx]),
ColorDyeTable dyeTable => dyeTable[rowIdx],
_ => throw new InvalidOperationException($"Unsupported color dye table type {Mtrl.DyeTable.GetType()}"),
};
if (dyeRow.Channel < StainService.ChannelCount)
{
StainId stainId = _stainService.GetStainCombo(dyeRow.Channel).CurrentSelection.Key;
if (_stainService.LegacyStmFile.TryGetValue(dyeRow.Template, stainId, out var legacyDyes))
row.ApplyDye(dyeRow, legacyDyes);
if (_stainService.GudStmFile.TryGetValue(dyeRow.Template, stainId, out var gudDyes))
row.ApplyDye(dyeRow, gudDyes);
}
}
if (HighlightedColorTablePair << 1 == rowIdx)
ApplyHighlight(ref row, ColorId.InGameHighlight, (float)HighlightTime.Elapsed.TotalSeconds);
else if (((HighlightedColorTablePair << 1) | 1) == rowIdx)
ApplyHighlight(ref row, ColorId.InGameHighlight2, (float)HighlightTime.Elapsed.TotalSeconds);
foreach (var previewer in ColorTablePreviewers)
{
row[..].CopyTo(previewer.GetColorRow(rowIdx));
previewer.ScheduleUpdate();
}
}
public void UpdateColorTablePreview()
{
if (ColorTablePreviewers.Count == 0)
return;
if (Mtrl.Table == null)
return;
var rows = new ColorTable(Mtrl.Table);
var dyeRows = Mtrl.DyeTable != null ? ColorDyeTable.CastOrConvert(Mtrl.DyeTable) : null;
if (dyeRows != null)
{
ReadOnlySpan<StainId> stainIds = [
_stainService.StainCombo1.CurrentSelection.Key,
_stainService.StainCombo2.CurrentSelection.Key,
];
rows.ApplyDye(_stainService.LegacyStmFile, stainIds, dyeRows);
rows.ApplyDye(_stainService.GudStmFile, stainIds, dyeRows);
}
if (HighlightedColorTablePair >= 0)
{
ApplyHighlight(ref rows[HighlightedColorTablePair << 1], ColorId.InGameHighlight, (float)HighlightTime.Elapsed.TotalSeconds);
ApplyHighlight(ref rows[(HighlightedColorTablePair << 1) | 1], ColorId.InGameHighlight2, (float)HighlightTime.Elapsed.TotalSeconds);
}
foreach (var previewer in ColorTablePreviewers)
{
rows.AsHalves().CopyTo(previewer.ColorTable);
previewer.ScheduleUpdate();
}
}
private static void ApplyHighlight(ref ColorTable.Row row, ColorId colorId, float time)
{
var level = (MathF.Sin(time * 2.0f * MathF.PI) + 2.0f) / 3.0f / 255.0f;
var baseColor = colorId.Value();
var color = level * new Vector3(baseColor & 0xFF, (baseColor >> 8) & 0xFF, (baseColor >> 16) & 0xFF);
var halfColor = (HalfColor)(color * color);
row.DiffuseColor = halfColor;
row.SpecularColor = halfColor;
row.EmissiveColor = halfColor;
}
}

View file

@ -0,0 +1,505 @@
using Dalamud.Interface;
using Dalamud.Interface.ImGuiNotification;
using ImGuiNET;
using Newtonsoft.Json.Linq;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii;
using OtterGui.Text;
using Penumbra.GameData;
using Penumbra.GameData.Data;
using Penumbra.GameData.Files;
using Penumbra.GameData.Files.ShaderStructs;
using Penumbra.String.Classes;
using static Penumbra.GameData.Files.ShpkFile;
namespace Penumbra.UI.AdvancedWindow.Materials;
public partial class MtrlTab
{
// 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 severe performance/memory issues when calculating the effective shader set
// 2. they probably aren't intended for use with materials anyway
internal static readonly IReadOnlyList<string> StandardShaderPackages = new[]
{
"3dui.shpk",
// "apricot_decal_dummy.shpk",
// "apricot_decal_ring.shpk",
// "apricot_decal.shpk",
// "apricot_fogModel.shpk",
// "apricot_gbuffer_decal_dummy.shpk",
// "apricot_gbuffer_decal_ring.shpk",
// "apricot_gbuffer_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",
"bg_composite.shpk",
"bgcrestchange.shpk",
"bgdecal.shpk",
"bgprop.shpk",
"bg.shpk",
"bguvscroll.shpk",
"characterglass.shpk",
"characterinc.shpk",
"characterlegacy.shpk",
"characterocclusion.shpk",
"characterreflection.shpk",
"characterscroll.shpk",
"charactershadowoffset.shpk",
"character.shpk",
"characterstockings.shpk",
"charactertattoo.shpk",
"charactertransparency.shpk",
"cloud.shpk",
"createviewposition.shpk",
"crystal.shpk",
"directionallighting.shpk",
"directionalshadow.shpk",
"furblur.shpk",
"grassdynamicwave.shpk",
"grass.shpk",
"hairmask.shpk",
"hair.shpk",
"iris.shpk",
"lightshaft.shpk",
"linelighting.shpk",
"planelighting.shpk",
"pointlighting.shpk",
"river.shpk",
"shadowmask.shpk",
"skin.shpk",
"spotlighting.shpk",
"subsurfaceblur.shpk",
"verticalfog.shpk",
"water.shpk",
"weather.shpk",
};
private static readonly byte[] UnknownShadersString = Encoding.UTF8.GetBytes("Vertex Shaders: ???\nPixel Shaders: ???");
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 bool ShpkLoading;
public JObject? AssociatedShpkDevkit;
public readonly string LoadedBaseDevkitPathName;
public readonly JObject? AssociatedBaseDevkit;
// Shader Key State
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;
public ReadOnlyMemory<byte> ShadersString = UnknownShadersString;
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 FullPath FindAssociatedShpk(out string defaultPath, out Utf8GamePath defaultGamePath)
{
defaultPath = GamePaths.Shader.ShpkPath(Mtrl.ShaderPackage.Name);
if (!Utf8GamePath.FromString(defaultPath, out defaultGamePath))
return FullPath.Empty;
return _edit.FindBestMatch(defaultGamePath);
}
public void LoadShpk(FullPath path)
=> Task.Run(() => DoLoadShpk(path));
private async Task DoLoadShpk(FullPath path)
{
ShadersKnown = false;
ShaderHeader = $"Shader ({Mtrl.ShaderPackage.Name})###Shader";
ShpkLoading = true;
try
{
var data = path.IsRooted
? await File.ReadAllBytesAsync(path.FullName)
: _gameData.GetFile(path.InternalName.ToString())?.Data;
LoadedShpkPath = path;
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;
Penumbra.Messager.NotificationMessage(e, $"Could not load {LoadedShpkPath.ToPath()}.", NotificationType.Error, false);
}
finally
{
ShpkLoading = false;
}
if (LoadedShpkPath.InternalName.IsEmpty)
{
AssociatedShpkDevkit = null;
LoadedShpkDevkitPathName = string.Empty;
}
else
{
AssociatedShpkDevkit =
TryLoadShpkDevkit(Path.GetFileNameWithoutExtension(Mtrl.ShaderPackage.Name), out LoadedShpkDevkitPathName);
}
UpdateShaderKeys();
_updateOnNextFrame = true;
}
private void UpdateShaderKeys()
{
ShaderKeys.Clear();
if (AssociatedShpk != null)
foreach (var key in AssociatedShpk.MaterialKeys)
{
var keyName = Names.KnownNames.TryResolve(key.Id);
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 valueKnownNames = keyName.WithKnownSuffixes();
var mtrlKeyIndex = Mtrl.FindOrAddShaderKey(key.Id, key.DefaultValue);
var values = valueSet.Select<uint, (string Label, uint Value, string Description)>(value =>
{
var valueName = valueKnownNames.TryResolve(Names.KnownNames, value);
if (dkData != null && dkData.Values.TryGetValue(value, out var dkValue))
return (dkValue.Label.Length > 0 ? dkValue.Label : valueName.ToString(), value, dkValue.Description);
return (valueName.ToString(), value, string.Empty);
}).ToArray();
Array.Sort(values, (x, y) =>
{
if (x.Value == key.DefaultValue)
return -1;
if (y.Value == key.DefaultValue)
return 1;
return string.Compare(x.Label, y.Label, StringComparison.Ordinal);
});
ShaderKeys.Add((hasDkLabel ? dkData!.Label : keyName.ToString(), mtrlKeyIndex, dkData?.Description ?? string.Empty,
!hasDkLabel, values));
}
else
foreach (var (key, index) in Mtrl.ShaderPackage.ShaderKeys.WithIndex())
{
var keyName = Names.KnownNames.TryResolve(key.Category);
var valueName = keyName.WithKnownSuffixes().TryResolve(Names.KnownNames, key.Value);
ShaderKeys.Add((keyName.ToString(), index, string.Empty, true, [(valueName.ToString(), key.Value, string.Empty)]));
}
}
private void UpdateShaders()
{
static void AddShader(HashSet<int> globalSet, Dictionary<uint, HashSet<int>> byPassSets, uint passId, int shaderIndex)
{
globalSet.Add(shaderIndex);
if (!byPassSets.TryGetValue(passId, out var passSet))
{
passSet = [];
byPassSets.Add(passId, passSet);
}
passSet.Add(shaderIndex);
}
VertexShaders.Clear();
PixelShaders.Clear();
var vertexShadersByPass = new Dictionary<uint, HashSet<int>>();
var pixelShadersByPass = new Dictionary<uint, HashSet<int>>();
if (AssociatedShpk == null || !AssociatedShpk.IsExhaustiveNodeAnalysisFeasible())
{
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)
{
var selector = BuildSelector(systemKeySelector, sceneKeySelector, materialKeySelector, subViewKeySelector);
var node = AssociatedShpk.GetNodeBySelector(selector);
if (node.HasValue)
foreach (var pass in node.Value.Passes)
{
AddShader(VertexShaders, vertexShadersByPass, pass.Id, (int)pass.VertexShader);
AddShader(PixelShaders, pixelShadersByPass, pass.Id, (int)pass.PixelShader);
}
else
ShadersKnown = false;
}
}
}
}
if (ShadersKnown)
{
var builder = new StringBuilder();
foreach (var (passId, passVS) in vertexShadersByPass)
{
if (builder.Length > 0)
builder.Append("\n\n");
var passName = Names.KnownNames.TryResolve(passId);
var shaders = passVS.OrderBy(i => i).Select(i => $"#{i}");
builder.Append($"Vertex Shaders ({passName}): {string.Join(", ", shaders)}");
if (pixelShadersByPass.TryGetValue(passId, out var passPS))
{
shaders = passPS.OrderBy(i => i).Select(i => $"#{i}");
builder.Append($"\nPixel Shaders ({passName}): {string.Join(", ", shaders)}");
}
}
foreach (var (passId, passPS) in pixelShadersByPass)
{
if (vertexShadersByPass.ContainsKey(passId))
continue;
if (builder.Length > 0)
builder.Append("\n\n");
var passName = Names.KnownNames.TryResolve(passId);
var shaders = passPS.OrderBy(i => i).Select(i => $"#{i}");
builder.Append($"Pixel Shaders ({passName}): {string.Join(", ", shaders)}");
}
ShadersString = Encoding.UTF8.GetBytes(builder.ToString());
}
else
ShadersString = UnknownShadersString;
ShaderComment = TryGetShpkDevkitData<string>("Comment", null, true) ?? string.Empty;
}
private bool DrawShaderSection(bool disabled)
{
var ret = false;
if (ImGui.CollapsingHeader(ShaderHeader))
{
ret |= DrawPackageNameInput(disabled);
ret |= DrawShaderFlagsInput(disabled);
DrawCustomAssociations();
ret |= DrawMaterialShaderKeys(disabled);
DrawMaterialShaders();
}
if (!ShpkLoading && (AssociatedShpk == null || AssociatedShpkDevkit == null))
{
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
if (AssociatedShpk == null)
{
ImUtf8.Text("Unable to find a suitable shader (.shpk) file for cross-references. Some functionality will be missing."u8,
ImGuiUtil.HalfBlendText(0x80u)); // Half red
}
else
{
ImUtf8.Text("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."u8,
ImGuiUtil.HalfBlendText(0x8080u)); // Half yellow
}
}
return ret;
}
private bool DrawPackageNameInput(bool disabled)
{
if (disabled)
{
ImGui.TextUnformatted("Shader Package: " + Mtrl.ShaderPackage.Name);
return false;
}
var ret = false;
ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f);
using var c = ImRaii.Combo("Shader Package", Mtrl.ShaderPackage.Name);
if (c)
foreach (var value in GetShpkNames())
{
if (ImGui.Selectable(value, value == Mtrl.ShaderPackage.Name))
{
Mtrl.ShaderPackage.Name = value;
ret = true;
AssociatedShpk = null;
LoadedShpkPath = FullPath.Empty;
LoadShpk(FindAssociatedShpk(out _, out _));
}
}
return ret;
}
private bool DrawShaderFlagsInput(bool disabled)
{
var shpkFlags = (int)Mtrl.ShaderPackage.Flags;
ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f);
if (!ImGui.InputInt("Shader Flags", ref shpkFlags, 0, 0,
ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)))
return false;
Mtrl.ShaderPackage.Flags = (uint)shpkFlags;
SetShaderPackageFlags((uint)shpkFlags);
return true;
}
/// <summary>
/// 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.
/// </summary>
private void DrawCustomAssociations()
{
const string tooltip = "Click to copy file path to clipboard.";
var text = AssociatedShpk == null
? "Associated .shpk file: None"
: $"Associated .shpk file: {LoadedShpkPathName}";
var devkitText = AssociatedShpkDevkit == null
? "Associated dev-kit file: None"
: $"Associated dev-kit file: {LoadedShpkDevkitPathName}";
var baseDevkitText = AssociatedBaseDevkit == null
? "Base dev-kit file: None"
: $"Base dev-kit file: {LoadedBaseDevkitPathName}";
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
ImGuiUtil.CopyOnClickSelectable(text, LoadedShpkPathName, tooltip);
ImGuiUtil.CopyOnClickSelectable(devkitText, LoadedShpkDevkitPathName, tooltip);
ImGuiUtil.CopyOnClickSelectable(baseDevkitText, LoadedBaseDevkitPathName, tooltip);
if (ImGui.Button("Associate Custom .shpk File"))
_fileDialog.OpenFilePicker("Associate Custom .shpk File...", ".shpk", (success, name) =>
{
if (success)
LoadShpk(new FullPath(name[0]));
}, 1, _edit.Mod!.ModPath.FullName, false);
var moddedPath = FindAssociatedShpk(out var defaultPath, out var gamePath);
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton("Associate Default .shpk File", Vector2.Zero, moddedPath.ToPath(),
moddedPath.Equals(LoadedShpkPath)))
LoadShpk(moddedPath);
if (!gamePath.Path.Equals(moddedPath.InternalName))
{
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton("Associate Unmodded .shpk File", Vector2.Zero, defaultPath,
gamePath.Path.Equals(LoadedShpkPath.InternalName)))
LoadShpk(new FullPath(gamePath));
}
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
}
private bool DrawMaterialShaderKeys(bool disabled)
{
if (ShaderKeys.Count == 0)
return false;
var ret = false;
foreach (var (label, index, description, monoFont, values) in ShaderKeys)
{
using var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont);
ref var key = ref Mtrl.ShaderPackage.ShaderKeys[index];
var shpkKey = 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;
Update();
}
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;
}
private void DrawMaterialShaders()
{
if (AssociatedShpk == null)
return;
using (var node = ImUtf8.TreeNode("Candidate Shaders"u8))
{
if (node)
ImUtf8.Text(ShadersString.Span);
}
if (ShaderComment.Length > 0)
{
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
ImGui.TextUnformatted(ShaderComment);
}
}
}

View file

@ -0,0 +1,276 @@
using Dalamud.Interface;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Text;
using Penumbra.GameData;
using Penumbra.GameData.Files.MaterialStructs;
using Penumbra.String.Classes;
using static Penumbra.GameData.Files.MaterialStructs.SamplerFlags;
using static Penumbra.GameData.Files.ShpkFile;
namespace Penumbra.UI.AdvancedWindow.Materials;
public partial class MtrlTab
{
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;
private void UpdateTextures()
{
Textures.Clear();
SamplerIds.Clear();
if (AssociatedShpk == null)
{
SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId));
if (Mtrl.Table != null)
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)
{
SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId));
if (Mtrl.Table != null)
SamplerIds.Add(TableSamplerId);
}
foreach (var samplerId in SamplerIds)
{
var shpkSampler = AssociatedShpk.GetSamplerById(samplerId);
if (shpkSampler is not { 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.Table ??= new ColorTable();
}
Textures.Sort((x, y) => string.CompareOrdinal(x.Label, y.Label));
TextureLabelWidth = 50f * UiHelpers.Scale;
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));
}
}
TextureLabelWidth = TextureLabelWidth / UiHelpers.Scale + 4;
}
private static ReadOnlySpan<byte> TextureAddressModeTooltip(TextureAddressMode addressMode)
=> addressMode switch
{
TextureAddressMode.Wrap => "Tile the texture at every UV integer junction.\n\nFor example, for U values between 0 and 3, the texture is repeated three times."u8,
TextureAddressMode.Mirror => "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."u8,
TextureAddressMode.Clamp => "Texture coordinates outside the range [0.0, 1.0] are set to the texture color at 0.0 or 1.0, respectively."u8,
TextureAddressMode.Border => "Texture coordinates outside the range [0.0, 1.0] are set to the border color (generally black)."u8,
_ => ""u8,
};
private bool DrawTextureSection(bool disabled)
{
if (Textures.Count == 0)
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, TextureLabelWidth * UiHelpers.Scale);
foreach (var (label, textureI, samplerI, description, monoFont) in Textures)
{
using var _ = ImRaii.PushId(samplerI);
var tmp = Mtrl.Textures[textureI].Path;
var unfolded = 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)
UnfoldedTextures.Add(samplerI);
else
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 != Mtrl.Textures[textureI].Path)
{
ret = true;
Mtrl.Textures[textureI].Path = tmp;
}
ImGui.TableNextColumn();
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(disabled, textureI, samplerI);
ImGui.TableNextColumn();
}
}
return ret;
}
private static bool ComboTextureAddressMode(ReadOnlySpan<byte> label, ref TextureAddressMode value)
{
using var c = ImUtf8.Combo(label, value.ToString());
if (!c)
return false;
var ret = false;
foreach (var mode in Enum.GetValues<TextureAddressMode>())
{
if (ImGui.Selectable(mode.ToString(), mode == value))
{
value = mode;
ret = true;
}
ImUtf8.SelectableHelpMarker(TextureAddressModeTooltip(mode));
}
return ret;
}
private bool DrawMaterialSampler(bool disabled, int textureIdx, int samplerIdx)
{
var ret = false;
ref var texture = ref Mtrl.Textures[textureIdx];
ref var sampler = ref Mtrl.ShaderPackage.Samplers[samplerIdx];
var dx11 = texture.DX11;
if (ImUtf8.Checkbox("Prepend -- to the file name on DirectX 11"u8, ref dx11))
{
texture.DX11 = dx11;
ret = true;
}
ref var samplerFlags = ref SamplerFlags.Wrap(ref sampler.Flags);
ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f);
var addressMode = samplerFlags.UAddressMode;
if (ComboTextureAddressMode("##UAddressMode"u8, ref addressMode))
{
samplerFlags.UAddressMode = addressMode;
ret = true;
SetSamplerFlags(sampler.SamplerId, sampler.Flags);
}
ImGui.SameLine();
ImUtf8.LabeledHelpMarker("U Address Mode"u8, "Method to use for resolving a U texture coordinate that is outside the 0 to 1 range.");
ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f);
addressMode = samplerFlags.VAddressMode;
if (ComboTextureAddressMode("##VAddressMode"u8, ref addressMode))
{
samplerFlags.VAddressMode = addressMode;
ret = true;
SetSamplerFlags(sampler.SamplerId, sampler.Flags);
}
ImGui.SameLine();
ImUtf8.LabeledHelpMarker("V Address Mode"u8, "Method to use for resolving a V texture coordinate that is outside the 0 to 1 range.");
var lodBias = samplerFlags.LodBias;
ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f);
if (ImUtf8.DragScalar("##LoDBias"u8, ref lodBias, -8.0f, 7.984375f, 0.1f))
{
samplerFlags.LodBias = lodBias;
ret = true;
SetSamplerFlags(sampler.SamplerId, sampler.Flags);
}
ImGui.SameLine();
ImUtf8.LabeledHelpMarker("Level of Detail Bias"u8,
"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 = samplerFlags.MinLod;
ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f);
if (ImUtf8.DragScalar("##MinLoD"u8, ref minLod, 0, 15, 0.1f))
{
samplerFlags.MinLod = minLod;
ret = true;
SetSamplerFlags(sampler.SamplerId, sampler.Flags);
}
ImGui.SameLine();
ImUtf8.LabeledHelpMarker("Minimum Level of Detail"u8,
"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 = ImUtf8.TreeNode("Advanced Settings"u8);
if (!t)
return ret;
ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f);
if (ImUtf8.InputScalar("Texture Flags"u8, ref texture.Flags, "%04X"u8,
flags: disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))
ret = true;
ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f);
if (ImUtf8.InputScalar("Sampler Flags"u8, ref sampler.Flags, "%08X"u8,
flags: ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)))
{
ret = true;
SetSamplerFlags(sampler.SamplerId, sampler.Flags);
}
return ret;
}
}

View file

@ -0,0 +1,199 @@
using Dalamud.Plugin.Services;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Text;
using OtterGui.Widgets;
using Penumbra.GameData;
using Penumbra.GameData.Files;
using Penumbra.GameData.Files.MaterialStructs;
using Penumbra.GameData.Interop;
using Penumbra.Interop.Hooks.Objects;
using Penumbra.Interop.ResourceTree;
using Penumbra.Services;
using Penumbra.String;
using Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow.Materials;
public sealed partial class MtrlTab : IWritable, IDisposable
{
private const int ShpkPrefixLength = 16;
private static readonly CiByteString ShpkPrefix = CiByteString.FromSpanUnsafe("shader/sm5/shpk/"u8, true, true, true);
private readonly IDataManager _gameData;
private readonly IFramework _framework;
private readonly ObjectManager _objects;
private readonly CharacterBaseDestructor _characterBaseDestructor;
private readonly StainService _stainService;
private readonly ResourceTreeFactory _resourceTreeFactory;
private readonly FileDialogService _fileDialog;
private readonly MaterialTemplatePickers _materialTemplatePickers;
private readonly Configuration _config;
private readonly ModEditWindow _edit;
public readonly MtrlFile Mtrl;
public readonly string FilePath;
public readonly bool Writable;
private bool _updateOnNextFrame;
public unsafe MtrlTab(IDataManager gameData, IFramework framework, ObjectManager objects, CharacterBaseDestructor characterBaseDestructor,
StainService stainService, ResourceTreeFactory resourceTreeFactory, FileDialogService fileDialog, MaterialTemplatePickers materialTemplatePickers,
Configuration config, ModEditWindow edit, MtrlFile file, string filePath, bool writable)
{
_gameData = gameData;
_framework = framework;
_objects = objects;
_characterBaseDestructor = characterBaseDestructor;
_stainService = stainService;
_resourceTreeFactory = resourceTreeFactory;
_fileDialog = fileDialog;
_materialTemplatePickers = materialTemplatePickers;
_config = config;
_edit = edit;
Mtrl = file;
FilePath = filePath;
Writable = writable;
AssociatedBaseDevkit = TryLoadShpkDevkit("_base", out LoadedBaseDevkitPathName);
Update();
LoadShpk(FindAssociatedShpk(out _, out _));
if (writable)
{
_characterBaseDestructor.Subscribe(UnbindFromDrawObjectMaterialInstances, CharacterBaseDestructor.Priority.MtrlTab);
BindToMaterialInstances();
}
}
public bool DrawVersionUpdate(bool disabled)
{
if (disabled || Mtrl.IsDawntrail)
return false;
if (!ImUtf8.ButtonEx("Update MTRL Version to Dawntrail"u8,
"Try using this if the material can not be loaded or should use legacy shaders.\n\nThis is not revertible."u8,
new Vector2(-0.1f, 0), false, 0, Colors.PressEnterWarningBg))
return false;
Mtrl.MigrateToDawntrail();
Update();
LoadShpk(FindAssociatedShpk(out _, out _));
return true;
}
public bool DrawPanel(bool disabled)
{
if (_updateOnNextFrame)
{
_updateOnNextFrame = false;
Update();
}
DrawMaterialLivePreviewRebind(disabled);
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
var ret = DrawBackFaceAndTransparency(disabled);
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
ret |= DrawShaderSection(disabled);
ret |= DrawTextureSection(disabled);
ret |= DrawColorTableSection(disabled);
ret |= DrawConstantsSection(disabled);
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
DrawOtherMaterialDetails(disabled);
return !disabled && ret;
}
private bool DrawBackFaceAndTransparency(bool disabled)
{
ref var shaderFlags = ref ShaderFlags.Wrap(ref Mtrl.ShaderPackage.Flags);
var ret = false;
using var dis = ImRaii.Disabled(disabled);
var tmp = shaderFlags.EnableTransparency;
if (ImGui.Checkbox("Enable Transparency", ref tmp))
{
shaderFlags.EnableTransparency = tmp;
ret = true;
SetShaderPackageFlags(Mtrl.ShaderPackage.Flags);
}
ImGui.SameLine(200 * UiHelpers.Scale + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X);
tmp = shaderFlags.HideBackfaces;
if (ImGui.Checkbox("Hide Backfaces", ref tmp))
{
shaderFlags.HideBackfaces = tmp;
ret = true;
SetShaderPackageFlags(Mtrl.ShaderPackage.Flags);
}
if (ShpkLoading)
{
ImGui.SameLine(400 * UiHelpers.Scale + 2 * ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X);
ImUtf8.Text("Loading shader (.shpk) file. Some functionality will only be available after this is done."u8,
ImGuiUtil.HalfBlendText(0x808000u)); // Half cyan
}
return ret;
}
private void DrawOtherMaterialDetails(bool _)
{
if (!ImGui.CollapsingHeader("Further Content"))
return;
using (var sets = ImRaii.TreeNode("UV Sets", ImGuiTreeNodeFlags.DefaultOpen))
{
if (sets)
foreach (var set in Mtrl.UvSets)
ImRaii.TreeNode($"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf).Dispose();
}
using (var sets = ImRaii.TreeNode("Color Sets", ImGuiTreeNodeFlags.DefaultOpen))
{
if (sets)
foreach (var set in Mtrl.ColorSets)
ImRaii.TreeNode($"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf).Dispose();
}
if (Mtrl.AdditionalData.Length <= 0)
return;
using var t = ImRaii.TreeNode($"Additional Data (Size: {Mtrl.AdditionalData.Length})###AdditionalData");
if (t)
Widget.DrawHexViewer(Mtrl.AdditionalData);
}
public void Update()
{
UpdateShaders();
UpdateTextures();
UpdateConstants();
}
public unsafe void Dispose()
{
UnbindFromMaterialInstances();
if (Writable)
_characterBaseDestructor.Unsubscribe(UnbindFromDrawObjectMaterialInstances);
}
public bool Valid
=> ShadersKnown && Mtrl.Valid;
public byte[] Write()
{
var output = Mtrl.Clone();
output.GarbageCollect(AssociatedShpk, SamplerIds);
return output.Write();
}
}

View file

@ -0,0 +1,18 @@
using Dalamud.Plugin.Services;
using OtterGui.Services;
using Penumbra.GameData.Files;
using Penumbra.GameData.Interop;
using Penumbra.Interop.Hooks.Objects;
using Penumbra.Interop.ResourceTree;
using Penumbra.Services;
namespace Penumbra.UI.AdvancedWindow.Materials;
public sealed class MtrlTabFactory(IDataManager gameData, IFramework framework, ObjectManager objects,
CharacterBaseDestructor characterBaseDestructor, StainService stainService, ResourceTreeFactory resourceTreeFactory,
FileDialogService fileDialog, MaterialTemplatePickers materialTemplatePickers, Configuration config) : IUiService
{
public MtrlTab Create(ModEditWindow edit, MtrlFile file, string filePath, bool writable)
=> new(gameData, framework, objects, characterBaseDestructor, stainService, resourceTreeFactory, fileDialog,
materialTemplatePickers, config, edit, file, filePath, writable);
}

View file

@ -1,538 +0,0 @@
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Files;
using Penumbra.GameData.Files.MaterialStructs;
using Penumbra.String.Functions;
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 DrawMaterialColorTableChange(MtrlTab tab, bool disabled)
{
if (!tab.SamplerIds.Contains(ShpkFile.TableSamplerId) || !tab.Mtrl.HasTable)
return false;
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
if (!ImGui.CollapsingHeader("Color Table", ImGuiTreeNodeFlags.DefaultOpen))
return false;
ColorTableCopyAllClipboardButton(tab.Mtrl);
ImGui.SameLine();
var ret = ColorTablePasteAllClipboardButton(tab, disabled);
if (!disabled)
{
ImGui.SameLine();
ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0));
ImGui.SameLine();
ret |= ColorTableDyeableCheckbox(tab);
}
var hasDyeTable = tab.Mtrl.HasDyeTable;
if (hasDyeTable)
{
ImGui.SameLine();
ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0));
ImGui.SameLine();
ret |= DrawPreviewDye(tab, disabled);
}
using var table = ImRaii.Table("##ColorTable", hasDyeTable ? 11 : 9,
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");
if (hasDyeTable)
{
ImGui.TableNextColumn();
ImGui.TableHeader("Dye");
ImGui.TableNextColumn();
ImGui.TableHeader("Dye Preview");
}
for (var i = 0; i < ColorTable.NumRows; ++i)
{
ret |= DrawColorTableRow(tab, i, disabled);
ImGui.TableNextRow();
}
return ret;
}
private static void ColorTableCopyAllClipboardButton(MtrlFile file)
{
if (!ImGui.Button("Export All Rows to Clipboard", ImGuiHelpers.ScaledVector2(200, 0)))
return;
try
{
var data1 = file.Table.AsBytes();
var data2 = file.HasDyeTable ? file.DyeTable.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 bool DrawPreviewDye(MtrlTab tab, bool disabled)
{
var (dyeId, (name, dyeColor, gloss)) = _stainService.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;
if (tab.Mtrl.HasDyeTable)
for (var i = 0; i < LegacyColorTable.NumUsedRows; ++i)
ret |= tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, i, dyeId, 0);
tab.UpdateColorTablePreview();
return ret;
}
ImGui.SameLine();
var label = dyeId == 0 ? "Preview Dye###previewDye" : $"{name} (Preview)###previewDye";
if (_stainService.StainCombo.Draw(label, dyeColor, string.Empty, true, gloss))
tab.UpdateColorTablePreview();
return false;
}
private static unsafe bool ColorTablePasteAllClipboardButton(MtrlTab tab, bool disabled)
{
if (!ImGuiUtil.DrawDisabledButton("Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2(200, 0), string.Empty, disabled)
|| !tab.Mtrl.HasTable)
return false;
try
{
var text = ImGui.GetClipboardText();
var data = Convert.FromBase64String(text);
if (data.Length < Marshal.SizeOf<ColorTable>())
return false;
ref var rows = ref tab.Mtrl.Table;
fixed (void* ptr = data, output = &rows)
{
MemoryUtility.MemCpyUnchecked(output, ptr, Marshal.SizeOf<ColorTable>());
if (data.Length >= Marshal.SizeOf<ColorTable>() + Marshal.SizeOf<ColorDyeTable>()
&& tab.Mtrl.HasDyeTable)
{
ref var dyeRows = ref tab.Mtrl.DyeTable;
fixed (void* output2 = &dyeRows)
{
MemoryUtility.MemCpyUnchecked(output2, (byte*)ptr + Marshal.SizeOf<ColorTable>(),
Marshal.SizeOf<ColorDyeTable>());
}
}
}
tab.UpdateColorTablePreview();
return true;
}
catch
{
return false;
}
}
[SkipLocalsInit]
private static unsafe void ColorTableCopyClipboardButton(ColorTable.Row row, ColorDyeTable.Row dye)
{
if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), ImGui.GetFrameHeight() * Vector2.One,
"Export this row to your clipboard.", false, true))
return;
try
{
Span<byte> data = stackalloc byte[ColorTable.Row.Size + ColorDyeTable.Row.Size];
fixed (byte* ptr = data)
{
MemoryUtility.MemCpyUnchecked(ptr, &row, ColorTable.Row.Size);
MemoryUtility.MemCpyUnchecked(ptr + ColorTable.Row.Size, &dye, ColorDyeTable.Row.Size);
}
var text = Convert.ToBase64String(data);
ImGui.SetClipboardText(text);
}
catch
{
// ignored
}
}
private static bool ColorTableDyeableCheckbox(MtrlTab tab)
{
var dyeable = tab.Mtrl.HasDyeTable;
var ret = ImGui.Checkbox("Dyeable", ref dyeable);
if (ret)
{
tab.Mtrl.HasDyeTable = dyeable;
tab.UpdateColorTablePreview();
}
return ret;
}
private static unsafe bool ColorTablePasteFromClipboardButton(MtrlTab tab, 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))
return false;
try
{
var text = ImGui.GetClipboardText();
var data = Convert.FromBase64String(text);
if (data.Length != ColorTable.Row.Size + ColorDyeTable.Row.Size
|| !tab.Mtrl.HasTable)
return false;
fixed (byte* ptr = data)
{
tab.Mtrl.Table[rowIdx] = *(ColorTable.Row*)ptr;
if (tab.Mtrl.HasDyeTable)
tab.Mtrl.DyeTable[rowIdx] = *(ColorDyeTable.Row*)(ptr + ColorTable.Row.Size);
}
tab.UpdateColorTableRowPreview(rowIdx);
return true;
}
catch
{
return false;
}
}
private static void ColorTableHighlightButton(MtrlTab tab, int rowIdx, bool disabled)
{
ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Crosshairs.ToIconString(), ImGui.GetFrameHeight() * Vector2.One,
"Highlight this row on your character, if possible.", disabled || tab.ColorTablePreviewers.Count == 0, true);
if (ImGui.IsItemHovered())
tab.HighlightColorTableRow(rowIdx);
else if (tab.HighlightedColorTableRow == rowIdx)
tab.CancelColorTableHighlight();
}
private bool DrawColorTableRow(MtrlTab tab, 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);
ref var row = ref tab.Mtrl.Table[rowIdx];
var hasDye = tab.Mtrl.HasDyeTable;
ref var dye = ref tab.Mtrl.DyeTable[rowIdx];
var floatSize = 70 * UiHelpers.Scale;
var intSize = 45 * UiHelpers.Scale;
ImGui.TableNextColumn();
ColorTableCopyClipboardButton(row, dye);
ImGui.SameLine();
var ret = ColorTablePasteFromClipboardButton(tab, rowIdx, disabled);
ImGui.SameLine();
ColorTableHighlightButton(tab, 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 =>
{
tab.Mtrl.Table[rowIdx].Diffuse = c;
tab.UpdateColorTableRowPreview(rowIdx);
});
if (hasDye)
{
ImGui.SameLine();
ret |= ImGuiUtil.Checkbox("##dyeDiffuse", "Apply Diffuse Color on Dye", dye.Diffuse,
b =>
{
tab.Mtrl.DyeTable[rowIdx].Diffuse = b;
tab.UpdateColorTableRowPreview(rowIdx);
}, ImGuiHoveredFlags.AllowWhenDisabled);
}
ImGui.TableNextColumn();
ret |= ColorPicker("##Specular", "Specular Color", row.Specular, c =>
{
tab.Mtrl.Table[rowIdx].Specular = c;
tab.UpdateColorTableRowPreview(rowIdx);
});
ImGui.SameLine();
var tmpFloat = row.SpecularStrength;
ImGui.SetNextItemWidth(floatSize);
if (ImGui.DragFloat("##SpecularStrength", ref tmpFloat, 0.01f, 0f, HalfMaxValue, "%.2f")
&& FixFloat(ref tmpFloat, row.SpecularStrength))
{
row.SpecularStrength = tmpFloat;
ret = true;
tab.UpdateColorTableRowPreview(rowIdx);
}
ImGuiUtil.HoverTooltip("Specular Strength", ImGuiHoveredFlags.AllowWhenDisabled);
if (hasDye)
{
ImGui.SameLine();
ret |= ImGuiUtil.Checkbox("##dyeSpecular", "Apply Specular Color on Dye", dye.Specular,
b =>
{
tab.Mtrl.DyeTable[rowIdx].Specular = b;
tab.UpdateColorTableRowPreview(rowIdx);
}, ImGuiHoveredFlags.AllowWhenDisabled);
ImGui.SameLine();
ret |= ImGuiUtil.Checkbox("##dyeSpecularStrength", "Apply Specular Strength on Dye", dye.SpecularStrength,
b =>
{
tab.Mtrl.DyeTable[rowIdx].SpecularStrength = b;
tab.UpdateColorTableRowPreview(rowIdx);
}, ImGuiHoveredFlags.AllowWhenDisabled);
}
ImGui.TableNextColumn();
ret |= ColorPicker("##Emissive", "Emissive Color", row.Emissive, c =>
{
tab.Mtrl.Table[rowIdx].Emissive = c;
tab.UpdateColorTableRowPreview(rowIdx);
});
if (hasDye)
{
ImGui.SameLine();
ret |= ImGuiUtil.Checkbox("##dyeEmissive", "Apply Emissive Color on Dye", dye.Emissive,
b =>
{
tab.Mtrl.DyeTable[rowIdx].Emissive = b;
tab.UpdateColorTableRowPreview(rowIdx);
}, ImGuiHoveredFlags.AllowWhenDisabled);
}
ImGui.TableNextColumn();
tmpFloat = row.GlossStrength;
ImGui.SetNextItemWidth(floatSize);
var glossStrengthMin = ImGui.GetIO().KeyCtrl ? 0.0f : HalfEpsilon;
if (ImGui.DragFloat("##GlossStrength", ref tmpFloat, Math.Max(0.1f, tmpFloat * 0.025f), glossStrengthMin, HalfMaxValue, "%.1f")
&& FixFloat(ref tmpFloat, row.GlossStrength))
{
row.GlossStrength = Math.Max(tmpFloat, glossStrengthMin);
ret = true;
tab.UpdateColorTableRowPreview(rowIdx);
}
ImGuiUtil.HoverTooltip("Gloss Strength", ImGuiHoveredFlags.AllowWhenDisabled);
if (hasDye)
{
ImGui.SameLine();
ret |= ImGuiUtil.Checkbox("##dyeGloss", "Apply Gloss Strength on Dye", dye.Gloss,
b =>
{
tab.Mtrl.DyeTable[rowIdx].Gloss = b;
tab.UpdateColorTableRowPreview(rowIdx);
}, ImGuiHoveredFlags.AllowWhenDisabled);
}
ImGui.TableNextColumn();
int tmpInt = row.TileSet;
ImGui.SetNextItemWidth(intSize);
if (ImGui.DragInt("##TileSet", ref tmpInt, 0.25f, 0, 63) && tmpInt != row.TileSet && tmpInt is >= 0 and <= ushort.MaxValue)
{
row.TileSet = (ushort)Math.Clamp(tmpInt, 0, 63);
ret = true;
tab.UpdateColorTableRowPreview(rowIdx);
}
ImGuiUtil.HoverTooltip("Tile Set", ImGuiHoveredFlags.AllowWhenDisabled);
ImGui.TableNextColumn();
tmpFloat = row.MaterialRepeat.X;
ImGui.SetNextItemWidth(floatSize);
if (ImGui.DragFloat("##RepeatX", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f")
&& FixFloat(ref tmpFloat, row.MaterialRepeat.X))
{
row.MaterialRepeat = row.MaterialRepeat with { X = tmpFloat };
ret = true;
tab.UpdateColorTableRowPreview(rowIdx);
}
ImGuiUtil.HoverTooltip("Repeat X", ImGuiHoveredFlags.AllowWhenDisabled);
ImGui.SameLine();
tmpFloat = row.MaterialRepeat.Y;
ImGui.SetNextItemWidth(floatSize);
if (ImGui.DragFloat("##RepeatY", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f")
&& FixFloat(ref tmpFloat, row.MaterialRepeat.Y))
{
row.MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat };
ret = true;
tab.UpdateColorTableRowPreview(rowIdx);
}
ImGuiUtil.HoverTooltip("Repeat Y", ImGuiHoveredFlags.AllowWhenDisabled);
ImGui.TableNextColumn();
tmpFloat = row.MaterialSkew.X;
ImGui.SetNextItemWidth(floatSize);
if (ImGui.DragFloat("##SkewX", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") && FixFloat(ref tmpFloat, row.MaterialSkew.X))
{
row.MaterialSkew = row.MaterialSkew with { X = tmpFloat };
ret = true;
tab.UpdateColorTableRowPreview(rowIdx);
}
ImGuiUtil.HoverTooltip("Skew X", ImGuiHoveredFlags.AllowWhenDisabled);
ImGui.SameLine();
tmpFloat = row.MaterialSkew.Y;
ImGui.SetNextItemWidth(floatSize);
if (ImGui.DragFloat("##SkewY", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") && FixFloat(ref tmpFloat, row.MaterialSkew.Y))
{
row.MaterialSkew = row.MaterialSkew with { Y = tmpFloat };
ret = true;
tab.UpdateColorTableRowPreview(rowIdx);
}
ImGuiUtil.HoverTooltip("Skew Y", ImGuiHoveredFlags.AllowWhenDisabled);
if (hasDye)
{
ImGui.TableNextColumn();
if (_stainService.TemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, intSize
+ ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton))
{
dye.Template = _stainService.TemplateCombo.CurrentSelection;
ret = true;
tab.UpdateColorTableRowPreview(rowIdx);
}
ImGuiUtil.HoverTooltip("Dye Template", ImGuiHoveredFlags.AllowWhenDisabled);
ImGui.TableNextColumn();
ret |= DrawDyePreview(tab, rowIdx, disabled, dye, floatSize);
}
return ret;
}
private bool DrawDyePreview(MtrlTab tab, int rowIdx, bool disabled, ColorDyeTable.Row dye, float floatSize)
{
var stain = _stainService.StainCombo.CurrentSelection.Key;
if (stain == 0 || !_stainService.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 && tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, rowIdx, stain, 0);
if (ret)
tab.UpdateColorTableRowPreview(rowIdx);
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, values.Gloss, values.Gloss, "%.1f G");
ImGui.SameLine();
ImGui.SetNextItemWidth(floatSize);
ImGui.DragFloat("##specularStrength", ref values.SpecularPower, 0, values.SpecularPower, values.SpecularPower, "%.2f S");
return ret;
}
private static bool ColorPicker(string label, string tooltip, Vector3 input, Action<Vector3> setter, string letter = "")
{
var ret = false;
var inputSqrt = PseudoSqrtRgb(input);
var tmp = inputSqrt;
if (ImGui.ColorEdit3(label, ref tmp,
ImGuiColorEditFlags.NoInputs
| ImGuiColorEditFlags.DisplayRGB
| ImGuiColorEditFlags.InputRGB
| ImGuiColorEditFlags.NoTooltip
| ImGuiColorEditFlags.HDR)
&& tmp != inputSqrt)
{
setter(PseudoSquareRgb(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;
}
// Functions to deal with squared RGB values without making negatives useless.
private static float PseudoSquareRgb(float x)
=> x < 0.0f ? -(x * x) : x * x;
private static Vector3 PseudoSquareRgb(Vector3 vec)
=> new(PseudoSquareRgb(vec.X), PseudoSquareRgb(vec.Y), PseudoSquareRgb(vec.Z));
private static Vector4 PseudoSquareRgb(Vector4 vec)
=> new(PseudoSquareRgb(vec.X), PseudoSquareRgb(vec.Y), PseudoSquareRgb(vec.Z), vec.W);
private static float PseudoSqrtRgb(float x)
=> x < 0.0f ? -MathF.Sqrt(-x) : MathF.Sqrt(x);
internal static Vector3 PseudoSqrtRgb(Vector3 vec)
=> new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z));
private static Vector4 PseudoSqrtRgb(Vector4 vec)
=> new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z), vec.W);
}

View file

@ -1,247 +0,0 @@
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);
}
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)
{
var spacing = ImGui.GetStyle().ItemInnerSpacing.X;
var fieldWidth = (ImGui.CalcItemWidth() - (values.Length - 1) * spacing) / values.Length;
var ret = false;
// Not using DragScalarN because of _relativeSpeed and other points of lost flexibility.
for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx)
{
if (valueIdx > 0)
ImGui.SameLine(0.0f, spacing);
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)
{
var spacing = ImGui.GetStyle().ItemInnerSpacing.X;
var fieldWidth = (ImGui.CalcItemWidth() - (values.Length - 1) * spacing) / values.Length;
var ret = false;
// Not using DragScalarN because of _relativeSpeed and other points of lost flexibility.
for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx)
{
if (valueIdx > 0)
ImGui.SameLine(0.0f, spacing);
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)
{
switch (values.Length)
{
case 3:
{
var value = new Vector3(values);
if (_squaredRgb)
value = PseudoSqrtRgb(value);
if (!ImGui.ColorEdit3("##0", ref value, ImGuiColorEditFlags.Float | (_clamped ? 0 : ImGuiColorEditFlags.HDR)) || disabled)
return false;
if (_squaredRgb)
value = PseudoSquareRgb(value);
if (_clamped)
value = Vector3.Clamp(value, Vector3.Zero, Vector3.One);
value.CopyTo(values);
return true;
}
case 4:
{
var value = new Vector4(values);
if (_squaredRgb)
value = PseudoSqrtRgb(value);
if (!ImGui.ColorEdit4("##0", ref value,
ImGuiColorEditFlags.Float | ImGuiColorEditFlags.AlphaPreviewHalf | (_clamped ? 0 : ImGuiColorEditFlags.HDR))
|| disabled)
return false;
if (_squaredRgb)
value = PseudoSquareRgb(value);
if (_clamped)
value = Vector4.Clamp(value, Vector4.Zero, Vector4.One);
value.CopyTo(values);
return true;
}
default: return FloatConstantEditor.Default.Draw(values, disabled);
}
}
}
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)
{
var spacing = ImGui.GetStyle().ItemInnerSpacing.X;
var fieldWidth = (ImGui.CalcItemWidth() - (values.Length - 1) * spacing) / values.Length;
var ret = false;
for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx)
{
using var id = ImRaii.PushId(valueIdx);
if (valueIdx > 0)
ImGui.SameLine(0.0f, spacing);
ImGui.SetNextItemWidth(MathF.Round(fieldWidth * (valueIdx + 1)) - MathF.Round(fieldWidth * valueIdx));
var currentValue = values[valueIdx];
var currentLabel = _values.FirstOrNull(v => v.Value == currentValue)?.Label
?? currentValue.ToString(CultureInfo.CurrentCulture);
ret = disabled
? ImGui.InputText(string.Empty, ref currentLabel, (uint)currentLabel.Length, ImGuiInputTextFlags.ReadOnly)
: DrawCombo(currentLabel, ref values[valueIdx]);
}
return ret;
}
private bool DrawCombo(string label, ref float currentValue)
{
using var c = ImRaii.Combo(string.Empty, label);
if (!c)
return false;
var ret = false;
foreach (var (valueLabel, value, valueDescription) in _values)
{
if (ImGui.Selectable(valueLabel, value == currentValue))
{
currentValue = value;
ret = true;
}
if (valueDescription.Length > 0)
ImGuiUtil.SelectableHelpMarker(valueDescription);
}
return ret;
}
}
}

View file

@ -1,783 +0,0 @@
using Dalamud.Interface;
using Dalamud.Interface.ImGuiNotification;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using ImGuiNET;
using Newtonsoft.Json.Linq;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii;
using Penumbra.GameData.Data;
using Penumbra.GameData.Files;
using Penumbra.GameData.Files.MaterialStructs;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Hooks.Objects;
using Penumbra.Interop.MaterialPreview;
using Penumbra.String;
using Penumbra.String.Classes;
using Penumbra.UI.Classes;
using static Penumbra.GameData.Files.ShpkFile;
namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow
{
private sealed class MtrlTab : IWritable, IDisposable
{
private const int ShpkPrefixLength = 16;
private static readonly CiByteString ShpkPrefix = CiByteString.FromSpanUnsafe("shader/sm5/shpk/"u8, true, true, true);
private readonly ModEditWindow _edit;
public readonly MtrlFile Mtrl;
public readonly string FilePath;
public readonly bool Writable;
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 readonly string LoadedBaseDevkitPathName;
public readonly JObject? AssociatedBaseDevkit;
// Shader Key State
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;
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;
// Material Constants
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);
public readonly List<LiveColorTablePreviewer> ColorTablePreviewers = new(4);
public int HighlightedColorTableRow = -1;
public readonly Stopwatch HighlightTime = new();
public FullPath FindAssociatedShpk(out string defaultPath, out Utf8GamePath defaultGamePath)
{
defaultPath = GamePaths.Shader.ShpkPath(Mtrl.ShaderPackage.Name);
if (!Utf8GamePath.FromString(defaultPath, out defaultGamePath))
return FullPath.Empty;
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;
var data = LoadedShpkPath.IsRooted
? File.ReadAllBytes(LoadedShpkPath.FullName)
: _edit._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;
Penumbra.Messager.NotificationMessage(e, $"Could not load {LoadedShpkPath.ToPath()}.", NotificationType.Error, false);
}
if (LoadedShpkPath.InternalName.IsEmpty)
{
AssociatedShpkDevkit = null;
LoadedShpkDevkitPathName = string.Empty;
}
else
{
AssociatedShpkDevkit =
TryLoadShpkDevkit(Path.GetFileNameWithoutExtension(Mtrl.ShaderPackage.Name), out LoadedShpkDevkitPathName);
}
UpdateShaderKeys();
Update();
}
private JObject? TryLoadShpkDevkit(string shpkBaseName, out string devkitPathName)
{
try
{
if (!Utf8GamePath.FromString("penumbra/shpk_devkit/" + shpkBaseName + ".json", out var devkitPath))
throw new Exception("Could not assemble ShPk dev-kit path.");
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;
}
}
private T? TryGetShpkDevkitData<T>(string category, uint? id, bool mayVary) where T : class
=> TryGetShpkDevkitData<T>(AssociatedShpkDevkit, LoadedShpkDevkitPathName, category, id, mayVary)
?? TryGetShpkDevkitData<T>(AssociatedBaseDevkit, LoadedBaseDevkitPathName, category, id, mayVary);
private T? TryGetShpkDevkitData<T>(JObject? devkit, string devkitPathName, string category, uint? id, bool mayVary) where T : class
{
if (devkit == null)
return null;
try
{
var data = devkit[category];
if (id.HasValue)
data = data?[id.Value.ToString()];
if (mayVary && (data as JObject)?["Vary"] != null)
{
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];
}
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;
}
}
private void UpdateShaderKeys()
{
ShaderKeys.Clear();
if (AssociatedShpk != null)
foreach (var key in AssociatedShpk.MaterialKeys)
{
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 =>
{
if (dkData != null && dkData.Values.TryGetValue(value, out var dkValue))
return (dkValue.Label.Length > 0 ? dkValue.Label : $"0x{value:X8}", value, dkValue.Description);
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 string.Compare(x.Label, y.Label, StringComparison.Ordinal);
});
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)>()));
}
private 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)
{
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;
}
}
}
}
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;
}
private void UpdateTextures()
{
Textures.Clear();
SamplerIds.Clear();
if (AssociatedShpk == null)
{
SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId));
if (Mtrl.HasTable)
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)
{
SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId));
if (Mtrl.HasTable)
SamplerIds.Add(TableSamplerId);
}
foreach (var samplerId in SamplerIds)
{
var shpkSampler = AssociatedShpk.GetSamplerById(samplerId);
if (shpkSampler is not { 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.HasTable = true;
}
Textures.Sort((x, y) => string.CompareOrdinal(x.Label, y.Label));
TextureLabelWidth = 50f * UiHelpers.Scale;
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));
}
}
TextureLabelWidth = TextureLabelWidth / UiHelpers.Scale + 4;
}
private void UpdateConstants()
{
static List<T> FindOrAddGroup<T>(List<(string, List<T>)> groups, string name)
{
foreach (var (groupName, group) in groups)
{
if (string.Equals(name, groupName, StringComparison.Ordinal))
return group;
}
var newGroup = new List<T>(16);
groups.Add((name, newGroup));
return newGroup;
}
Constants.Clear();
if (AssociatedShpk == null)
{
var fcGroup = FindOrAddGroup(Constants, "Further Constants");
foreach (var (constant, index) in Mtrl.ShaderPackage.Constants.WithIndex())
{
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)
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);
}
var fcGroup = FindOrAddGroup(Constants, "Further Constants");
foreach (var (start, end) in handledElements.Ranges(complement: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()
{
UnbindFromMaterialInstances();
var instances = MaterialInfo.FindMaterials(_edit._resourceTreeFactory.GetLocalPlayerRelatedCharacters().Select(ch => ch.Address),
FilePath);
var foundMaterials = new HashSet<nint>();
foreach (var materialInfo in instances)
{
var material = materialInfo.GetDrawObjectMaterial(_edit._objects);
if (foundMaterials.Contains((nint)material))
continue;
try
{
MaterialPreviewers.Add(new LiveMaterialPreviewer(_edit._objects, materialInfo));
foundMaterials.Add((nint)material);
}
catch (InvalidOperationException)
{
// Carry on without that previewer.
}
}
UpdateMaterialPreview();
if (!Mtrl.HasTable)
return;
foreach (var materialInfo in instances)
{
try
{
ColorTablePreviewers.Add(new LiveColorTablePreviewer(_edit._objects, _edit._framework, materialInfo));
}
catch (InvalidOperationException)
{
// Carry on without that previewer.
}
}
UpdateColorTablePreview();
}
private void UnbindFromMaterialInstances()
{
foreach (var previewer in MaterialPreviewers)
previewer.Dispose();
MaterialPreviewers.Clear();
foreach (var previewer in ColorTablePreviewers)
previewer.Dispose();
ColorTablePreviewers.Clear();
}
private unsafe void UnbindFromDrawObjectMaterialInstances(CharacterBase* characterBase)
{
for (var i = MaterialPreviewers.Count; i-- > 0;)
{
var previewer = MaterialPreviewers[i];
if (previewer.DrawObject != characterBase)
continue;
previewer.Dispose();
MaterialPreviewers.RemoveAt(i);
}
for (var i = ColorTablePreviewers.Count; i-- > 0;)
{
var previewer = ColorTablePreviewers[i];
if (previewer.DrawObject != characterBase)
continue;
previewer.Dispose();
ColorTablePreviewers.RemoveAt(i);
}
}
public void SetShaderPackageFlags(uint shPkFlags)
{
foreach (var previewer in MaterialPreviewers)
previewer.SetShaderPackageFlags(shPkFlags);
}
public void SetMaterialParameter(uint parameterCrc, Index offset, Span<float> value)
{
foreach (var previewer in MaterialPreviewers)
previewer.SetMaterialParameter(parameterCrc, offset, value);
}
public void SetSamplerFlags(uint samplerCrc, uint samplerFlags)
{
foreach (var previewer in MaterialPreviewers)
previewer.SetSamplerFlags(samplerCrc, samplerFlags);
}
private 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 HighlightColorTableRow(int rowIdx)
{
var oldRowIdx = HighlightedColorTableRow;
if (HighlightedColorTableRow != rowIdx)
{
HighlightedColorTableRow = rowIdx;
HighlightTime.Restart();
}
if (oldRowIdx >= 0)
UpdateColorTableRowPreview(oldRowIdx);
if (rowIdx >= 0)
UpdateColorTableRowPreview(rowIdx);
}
public void CancelColorTableHighlight()
{
var rowIdx = HighlightedColorTableRow;
HighlightedColorTableRow = -1;
HighlightTime.Reset();
if (rowIdx >= 0)
UpdateColorTableRowPreview(rowIdx);
}
public void UpdateColorTableRowPreview(int rowIdx)
{
if (ColorTablePreviewers.Count == 0)
return;
if (!Mtrl.HasTable)
return;
var row = new LegacyColorTable.Row(Mtrl.Table[rowIdx]);
if (Mtrl.HasDyeTable)
{
var stm = _edit._stainService.StmFile;
var dye = new LegacyColorDyeTable.Row(Mtrl.DyeTable[rowIdx]);
if (stm.TryGetValue(dye.Template, _edit._stainService.StainCombo.CurrentSelection.Key, out var dyes))
row.ApplyDyeTemplate(dye, dyes);
}
if (HighlightedColorTableRow == rowIdx)
ApplyHighlight(ref row, (float)HighlightTime.Elapsed.TotalSeconds);
foreach (var previewer in ColorTablePreviewers)
{
row.AsHalves().CopyTo(previewer.ColorTable.AsSpan()
.Slice(LiveColorTablePreviewer.TextureWidth * 4 * rowIdx, LiveColorTablePreviewer.TextureWidth * 4));
previewer.ScheduleUpdate();
}
}
public void UpdateColorTablePreview()
{
if (ColorTablePreviewers.Count == 0)
return;
if (!Mtrl.HasTable)
return;
var rows = new LegacyColorTable(Mtrl.Table);
var dyeRows = new LegacyColorDyeTable(Mtrl.DyeTable);
if (Mtrl.HasDyeTable)
{
var stm = _edit._stainService.StmFile;
var stainId = (StainId)_edit._stainService.StainCombo.CurrentSelection.Key;
for (var i = 0; i < LegacyColorTable.NumUsedRows; ++i)
{
ref var row = ref rows[i];
var dye = dyeRows[i];
if (stm.TryGetValue(dye.Template, stainId, out var dyes))
row.ApplyDyeTemplate(dye, dyes);
}
}
if (HighlightedColorTableRow >= 0)
ApplyHighlight(ref rows[HighlightedColorTableRow], (float)HighlightTime.Elapsed.TotalSeconds);
foreach (var previewer in ColorTablePreviewers)
{
// TODO: Dawntrail
rows.AsHalves().CopyTo(previewer.ColorTable);
previewer.ScheduleUpdate();
}
}
private static void ApplyHighlight(ref LegacyColorTable.Row row, float time)
{
var level = (MathF.Sin(time * 2.0f * MathF.PI) + 2.0f) / 3.0f / 255.0f;
var baseColor = ColorId.InGameHighlight.Value();
var color = level * new Vector3(baseColor & 0xFF, (baseColor >> 8) & 0xFF, (baseColor >> 16) & 0xFF);
row.Diffuse = Vector3.Zero;
row.Specular = Vector3.Zero;
row.Emissive = color * color;
}
public void Update()
{
UpdateShaders();
UpdateTextures();
UpdateConstants();
}
public unsafe MtrlTab(ModEditWindow edit, MtrlFile file, string filePath, bool writable)
{
_edit = edit;
Mtrl = file;
FilePath = filePath;
Writable = writable;
AssociatedBaseDevkit = TryLoadShpkDevkit("_base", out LoadedBaseDevkitPathName);
LoadShpk(FindAssociatedShpk(out _, out _));
if (writable)
{
_edit._characterBaseDestructor.Subscribe(UnbindFromDrawObjectMaterialInstances, CharacterBaseDestructor.Priority.MtrlTab);
BindToMaterialInstances();
}
}
public unsafe void Dispose()
{
UnbindFromMaterialInstances();
if (Writable)
_edit._characterBaseDestructor.Unsubscribe(UnbindFromDrawObjectMaterialInstances);
}
// TODO Readd ShadersKnown
public bool Valid
=> (true || ShadersKnown) && Mtrl.Valid;
public byte[] Write()
{
var output = Mtrl.Clone();
output.GarbageCollect(AssociatedShpk, SamplerIds);
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;
}
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()
=> Type switch
{
DevkitConstantType.Hidden => null,
DevkitConstantType.Float => new FloatConstantEditor(Minimum, Maximum, Speed ?? 0.1f, RelativeSpeed, Factor, Bias, Precision,
Unit),
DevkitConstantType.Integer => new IntConstantEditor(ToInteger(Minimum), ToInteger(Maximum), Speed ?? 0.25f, RelativeSpeed,
Factor, Bias, Unit),
DevkitConstantType.Color => new ColorConstantEditor(SquaredRgb, Clamped),
DevkitConstantType.Enum => new EnumConstantEditor(Array.ConvertAll(Values,
value => (value.Label, value.Value, value.Description))),
_ => FloatConstantEditor.Default,
};
private static int? ToInteger(float? value)
=> value.HasValue ? (int)Math.Clamp(MathF.Round(value.Value), int.MinValue, int.MaxValue) : null;
}
}
}

View file

@ -1,481 +0,0 @@
using Dalamud.Interface;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData;
using Penumbra.String.Classes;
namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow
{
private readonly FileDialogService _fileDialog;
// 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[]
{
"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",
"charactershadowoffset.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[]
{
"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)
{
ImGui.TextUnformatted("Shader Package: " + tab.Mtrl.ShaderPackage.Name);
return false;
}
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;
}
private static bool DrawShaderFlagsInput(MtrlTab tab, bool disabled)
{
var shpkFlags = (int)tab.Mtrl.ShaderPackage.Flags;
ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f);
if (!ImGui.InputInt("Shader Flags", ref shpkFlags, 0, 0,
ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)))
return false;
tab.Mtrl.ShaderPackage.Flags = (uint)shpkFlags;
tab.SetShaderPackageFlags((uint)shpkFlags);
return true;
}
/// <summary>
/// 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.
/// </summary>
private void DrawCustomAssociations(MtrlTab tab)
{
const string tooltip = "Click to copy file path to clipboard.";
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));
ImGuiUtil.CopyOnClickSelectable(text, tab.LoadedShpkPathName, tooltip);
ImGuiUtil.CopyOnClickSelectable(devkitText, tab.LoadedShpkDevkitPathName, tooltip);
ImGuiUtil.CopyOnClickSelectable(baseDevkitText, tab.LoadedBaseDevkitPathName, tooltip);
if (ImGui.Button("Associate Custom .shpk File"))
_fileDialog.OpenFilePicker("Associate Custom .shpk File...", ".shpk", (success, name) =>
{
if (success)
tab.LoadShpk(new FullPath(name[0]));
}, 1, Mod!.ModPath.FullName, false);
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(tab.LoadedShpkPath)))
tab.LoadShpk(moddedPath);
if (!gamePath.Path.Equals(moddedPath.InternalName))
{
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton("Associate Unmodded .shpk File", Vector2.Zero, defaultPath,
gamePath.Path.Equals(tab.LoadedShpkPath.InternalName)))
tab.LoadShpk(new FullPath(gamePath));
}
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
}
private static bool DrawMaterialShaderKeys(MtrlTab tab, bool disabled)
{
if (tab.ShaderKeys.Count == 0)
return false;
var ret = false;
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 (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;
}
private static void DrawMaterialShaders(MtrlTab tab)
{
if (tab.AssociatedShpk == null)
return;
ImRaii.TreeNode(tab.VertexShadersString, ImGuiTreeNodeFlags.Leaf).Dispose();
ImRaii.TreeNode(tab.PixelShadersString, ImGuiTreeNodeFlags.Leaf).Dispose();
if (tab.ShaderComment.Length > 0)
{
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
ImGui.TextUnformatted(tab.ShaderComment);
}
}
private static bool DrawMaterialConstants(MtrlTab tab, bool disabled)
{
if (tab.Constants.Count == 0)
return false;
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
if (!ImGui.CollapsingHeader("Material Constants"))
return false;
using var _ = ImRaii.PushId("MaterialConstants");
var ret = false;
foreach (var (header, group) in tab.Constants)
{
using var t = ImRaii.TreeNode(header, ImGuiTreeNodeFlags.DefaultOpen);
if (!t)
continue;
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}");
ImGui.SetNextItemWidth(250.0f);
if (editor.Draw(buffer[slice], disabled))
{
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, int textureIdx, int samplerIdx)
{
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)
{
fixed (ushort* v2 = &v)
{
return ImGui.InputScalar(label, ImGuiDataType.U16, (nint)v2, nint.Zero, nint.Zero, "%04X", 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 * 100.0f);
if (ImGui.InputInt("Sampler Flags", ref samplerFlags, 0, 0,
ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)))
{
sampler.Flags = (uint)samplerFlags;
ret = true;
tab.SetSamplerFlags(sampler.SamplerId, (uint)samplerFlags);
}
return ret;
}
private bool DrawMaterialShader(MtrlTab tab, bool disabled)
{
var ret = false;
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.");
}
return ret;
}
private static string? MaterialParamName(bool componentOnly, int offset)
{
if (offset < 0)
return null;
return (componentOnly, offset & 0x3) switch
{
(true, 0) => "x",
(true, 1) => "y",
(true, 2) => "z",
(true, 3) => "w",
(false, 0) => $"[{offset >> 2:D2}].x",
(false, 1) => $"[{offset >> 2:D2}].y",
(false, 2) => $"[{offset >> 2:D2}].z",
(false, 3) => $"[{offset >> 2:D2}].w",
_ => 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)
{
if (valueLength == 0 || valueOffset < 0)
return (null, false);
var firstVector = valueOffset >> 2;
var lastVector = (valueOffset + valueLength - 1) >> 2;
var firstComponent = valueOffset & 0x3;
var lastComponent = (valueOffset + valueLength - 1) & 0x3;
if (firstVector == lastVector)
return ($"{prefix}[{firstVector}]{VectorSwizzle(firstComponent, lastComponent)}", true);
var sb = new StringBuilder(128);
sb.Append($"{prefix}[{firstVector}]{VectorSwizzle(firstComponent, 3).TrimEnd()}");
for (var i = firstVector + 1; i < lastVector; ++i)
sb.Append($", [{i}]");
sb.Append($", [{lastVector}]{VectorSwizzle(0, lastComponent)}");
return (sb.ToString(), false);
}
}

View file

@ -3,11 +3,7 @@ using Dalamud.Interface.Utility;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Text;
using OtterGui.Widgets;
using Penumbra.GameData.Files;
using Penumbra.String.Classes;
using Penumbra.UI.Classes;
using Penumbra.UI.AdvancedWindow.Materials;
namespace Penumbra.UI.AdvancedWindow;
@ -17,177 +13,10 @@ public partial class ModEditWindow
private bool DrawMaterialPanel(MtrlTab tab, bool disabled)
{
DrawVersionUpdate(tab, disabled);
DrawMaterialLivePreviewRebind(tab, disabled);
if (tab.DrawVersionUpdate(disabled))
_materialTab.SaveFile();
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
var ret = DrawBackFaceAndTransparency(tab, disabled);
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
ret |= DrawMaterialShader(tab, disabled);
ret |= DrawMaterialTextureChange(tab, disabled);
ret |= DrawMaterialColorTableChange(tab, disabled);
ret |= DrawMaterialConstants(tab, disabled);
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
DrawOtherMaterialDetails(tab.Mtrl, disabled);
return !disabled && ret;
}
private void DrawVersionUpdate(MtrlTab tab, bool disabled)
{
if (disabled || tab.Mtrl.IsDawnTrail)
return;
if (!ImUtf8.ButtonEx("Update MTRL Version to Dawntrail"u8,
"Try using this if the material can not be loaded or should use legacy shaders.\n\nThis is not revertible."u8,
new Vector2(-0.1f, 0), false, 0, Colors.PressEnterWarningBg))
return;
tab.Mtrl.MigrateToDawntrail();
_materialTab.SaveFile();
}
private static void DrawMaterialLivePreviewRebind(MtrlTab tab, bool disabled)
{
if (disabled)
return;
if (ImGui.Button("Reload live preview"))
tab.BindToMaterialInstances();
if (tab.MaterialPreviewers.Count != 0 || tab.ColorTablePreviewers.Count != 0)
return;
ImGui.SameLine();
using var c = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder);
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)
{
if (tab.Textures.Count == 0)
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);
foreach (var (label, textureI, samplerI, description, monoFont) in tab.Textures)
{
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[textureI].Path)
{
ret = true;
tab.Mtrl.Textures[textureI].Path = tmp;
}
ImGui.TableNextColumn();
using (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;
}
private static bool DrawBackFaceAndTransparency(MtrlTab tab, bool disabled)
{
const uint transparencyBit = 0x10;
const uint backfaceBit = 0x01;
var ret = false;
using var dis = ImRaii.Disabled(disabled);
var tmp = (tab.Mtrl.ShaderPackage.Flags & transparencyBit) != 0;
if (ImGui.Checkbox("Enable Transparency", ref tmp))
{
tab.Mtrl.ShaderPackage.Flags =
tmp ? tab.Mtrl.ShaderPackage.Flags | transparencyBit : tab.Mtrl.ShaderPackage.Flags & ~transparencyBit;
ret = true;
tab.SetShaderPackageFlags(tab.Mtrl.ShaderPackage.Flags);
}
ImGui.SameLine(200 * UiHelpers.Scale + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X);
tmp = (tab.Mtrl.ShaderPackage.Flags & backfaceBit) != 0;
if (ImGui.Checkbox("Hide Backfaces", ref tmp))
{
tab.Mtrl.ShaderPackage.Flags = tmp ? tab.Mtrl.ShaderPackage.Flags | backfaceBit : tab.Mtrl.ShaderPackage.Flags & ~backfaceBit;
ret = true;
tab.SetShaderPackageFlags(tab.Mtrl.ShaderPackage.Flags);
}
return ret;
}
private static void DrawOtherMaterialDetails(MtrlFile file, bool _)
{
if (!ImGui.CollapsingHeader("Further Content"))
return;
using (var sets = ImRaii.TreeNode("UV Sets", ImGuiTreeNodeFlags.DefaultOpen))
{
if (sets)
foreach (var set in file.UvSets)
ImRaii.TreeNode($"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf).Dispose();
}
using (var sets = ImRaii.TreeNode("Color Sets", ImGuiTreeNodeFlags.DefaultOpen))
{
if (sets)
foreach (var set in file.ColorSets)
ImRaii.TreeNode($"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf).Dispose();
}
if (file.AdditionalData.Length <= 0)
return;
using var t = ImRaii.TreeNode($"Additional Data (Size: {file.AdditionalData.Length})###AdditionalData");
if (t)
Widget.DrawHexViewer(file.AdditionalData);
return tab.DrawPanel(disabled);
}
private void DrawMaterialReassignmentTab()

View file

@ -15,6 +15,7 @@ namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow
{
private readonly FileDialogService _fileDialog;
private readonly ResourceTreeFactory _resourceTreeFactory;
private readonly ResourceTreeViewer _quickImportViewer;
private readonly Dictionary<FullPath, IWritable?> _quickImportWritables = new();

View file

@ -13,10 +13,8 @@ using Penumbra.Collections.Manager;
using Penumbra.Communication;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Files;
using Penumbra.GameData.Interop;
using Penumbra.Import.Models;
using Penumbra.Import.Textures;
using Penumbra.Interop.Hooks.Objects;
using Penumbra.Interop.ResourceTree;
using Penumbra.Meta;
using Penumbra.Mods;
@ -26,6 +24,7 @@ using Penumbra.Mods.SubMods;
using Penumbra.Services;
using Penumbra.String;
using Penumbra.String.Classes;
using Penumbra.UI.AdvancedWindow.Materials;
using Penumbra.UI.AdvancedWindow.Meta;
using Penumbra.UI.Classes;
using Penumbra.Util;
@ -39,20 +38,17 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
public readonly MigrationManager MigrationManager;
private readonly PerformanceTracker _performance;
private readonly ModEditor _editor;
private readonly Configuration _config;
private readonly ItemSwapTab _itemSwapTab;
private readonly MetaFileManager _metaFileManager;
private readonly ActiveCollections _activeCollections;
private readonly StainService _stainService;
private readonly ModMergeTab _modMergeTab;
private readonly CommunicatorService _communicator;
private readonly IDragDropManager _dragDropManager;
private readonly IDataManager _gameData;
private readonly IFramework _framework;
private readonly ObjectManager _objects;
private readonly CharacterBaseDestructor _characterBaseDestructor;
private readonly PerformanceTracker _performance;
private readonly ModEditor _editor;
private readonly Configuration _config;
private readonly ItemSwapTab _itemSwapTab;
private readonly MetaFileManager _metaFileManager;
private readonly ActiveCollections _activeCollections;
private readonly ModMergeTab _modMergeTab;
private readonly CommunicatorService _communicator;
private readonly IDragDropManager _dragDropManager;
private readonly IDataManager _gameData;
private readonly IFramework _framework;
private Vector2 _iconSize = Vector2.Zero;
private bool _allowReduplicate;
@ -541,7 +537,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
/// If none exists, goes through all options in the currently selected mod (if any) in order of priority and resolves in them.
/// If no redirection is found in either of those options, returns the original path.
/// </remarks>
private FullPath FindBestMatch(Utf8GamePath path)
internal FullPath FindBestMatch(Utf8GamePath path)
{
var currentFile = _activeCollections.Current.ResolvePath(path);
if (currentFile != null)
@ -562,7 +558,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
return new FullPath(path);
}
private HashSet<Utf8GamePath> FindPathsStartingWith(CiByteString prefix)
internal HashSet<Utf8GamePath> FindPathsStartingWith(CiByteString prefix)
{
var ret = new HashSet<Utf8GamePath>();
@ -587,34 +583,32 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, IDataManager gameData,
Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager,
StainService stainService, ActiveCollections activeCollections, ModMergeTab modMergeTab,
ActiveCollections activeCollections, ModMergeTab modMergeTab,
CommunicatorService communicator, TextureManager textures, ModelManager models, IDragDropManager dragDropManager,
ResourceTreeViewerFactory resourceTreeViewerFactory, ObjectManager objects, IFramework framework,
CharacterBaseDestructor characterBaseDestructor, MetaDrawers metaDrawers, MigrationManager migrationManager)
ResourceTreeViewerFactory resourceTreeViewerFactory, IFramework framework,
MetaDrawers metaDrawers, MigrationManager migrationManager,
MtrlTabFactory mtrlTabFactory)
: base(WindowBaseLabel)
{
_performance = performance;
_itemSwapTab = itemSwapTab;
_gameData = gameData;
_config = config;
_editor = editor;
_metaFileManager = metaFileManager;
_stainService = stainService;
_activeCollections = activeCollections;
_modMergeTab = modMergeTab;
_communicator = communicator;
_dragDropManager = dragDropManager;
_textures = textures;
_models = models;
_fileDialog = fileDialog;
_objects = objects;
_framework = framework;
_characterBaseDestructor = characterBaseDestructor;
MigrationManager = migrationManager;
_metaDrawers = metaDrawers;
_performance = performance;
_itemSwapTab = itemSwapTab;
_gameData = gameData;
_config = config;
_editor = editor;
_metaFileManager = metaFileManager;
_activeCollections = activeCollections;
_modMergeTab = modMergeTab;
_communicator = communicator;
_dragDropManager = dragDropManager;
_textures = textures;
_models = models;
_fileDialog = fileDialog;
_framework = framework;
MigrationManager = migrationManager;
_metaDrawers = metaDrawers;
_materialTab = new FileEditor<MtrlTab>(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl",
() => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => Mod?.ModPath.FullName ?? string.Empty,
(bytes, path, writable) => new MtrlTab(this, new MtrlFile(bytes), path, writable));
(bytes, path, writable) => mtrlTabFactory.Create(this, new MtrlFile(bytes), path, writable));
_modelTab = new FileEditor<MdlTab>(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Models", ".mdl",
() => PopulateIsOnPlayer(_editor.Files.Mdl, ResourceType.Mdl), DrawModelPanel, () => Mod?.ModPath.FullName ?? string.Empty,
(bytes, path, _) => new MdlTab(this, bytes, path));

View file

@ -24,6 +24,7 @@ public enum ColorId
NoAssignment,
SelectorPriority,
InGameHighlight,
InGameHighlight2,
ResTreeLocalPlayer,
ResTreePlayer,
ResTreeNetworked,
@ -70,7 +71,8 @@ public static class Colors
ColorId.NoModsAssignment => ( 0x50000080, "'Use No Mods' Collection Assignment", "A collection assignment set to not use any mods at all."),
ColorId.NoAssignment => ( 0x00000000, "Unassigned Collection Assignment", "A collection assignment that is not configured to any collection and thus just has no specific treatment."),
ColorId.SelectorPriority => ( 0xFF808080, "Mod Selector Priority", "The priority displayed for non-zero priority mods in the mod selector."),
ColorId.InGameHighlight => ( 0xFFEBCF89, "In-Game Highlight", "An in-game element that has been highlighted for ease of editing."),
ColorId.InGameHighlight => ( 0xFFEBCF89, "In-Game Highlight (Primary)", "An in-game element that has been highlighted for ease of editing."),
ColorId.InGameHighlight2 => ( 0xFF446CC0, "In-Game Highlight (Secondary)", "Another in-game element that has been highlighted for ease of editing."),
ColorId.ResTreeLocalPlayer => ( 0xFFFFE0A0, "On-Screen: You", "You and what you own (mount, minion, accessory, pets and so on), in the On-Screen tab." ),
ColorId.ResTreePlayer => ( 0xFFC0FFC0, "On-Screen: Other Players", "Other players and what they own, in the On-Screen tab." ),
ColorId.ResTreeNetworked => ( 0xFFFFFFFF, "On-Screen: Non-Players (Networked)", "Non-player entities handled by the game server, in the On-Screen tab." ),

View file

@ -327,6 +327,9 @@ public class SettingsTab : ITab, IUiService
UiHelpers.DefaultLineSpace();
DrawModHandlingSettings();
UiHelpers.DefaultLineSpace();
DrawModEditorSettings();
ImGui.NewLine();
}
@ -723,6 +726,15 @@ public class SettingsTab : ITab, IUiService
"Set the default Penumbra mod folder to place newly imported mods into.\nLeave blank to import into Root.");
}
/// <summary> Draw all settings pertaining to advanced editing of mods. </summary>
private void DrawModEditorSettings()
{
Checkbox("Advanced Editing: Edit Raw Tile UV Transforms",
"Edit the raw matrix components of tile UV transforms, instead of having them decomposed into scale, rotation and shear.",
_config.EditRawTileTransforms, v => _config.EditRawTileTransforms = v);
}
#endregion
/// <summary> Draw the entire Color subsection. </summary>