mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
DT material editor, main part
This commit is contained in:
parent
450751e43f
commit
36ab9573ae
21 changed files with 2744 additions and 2286 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
509
Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs
Normal file
509
Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs
Normal 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);
|
||||
}
|
||||
277
Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs
Normal file
277
Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
240
Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Devkit.cs
Normal file
240
Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Devkit.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
368
Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs
Normal file
368
Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
272
Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs
Normal file
272
Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
505
Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs
Normal file
505
Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
276
Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs
Normal file
276
Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
199
Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs
Normal file
199
Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
18
Penumbra/UI/AdvancedWindow/Materials/MtrlTabFactory.cs
Normal file
18
Penumbra/UI/AdvancedWindow/Materials/MtrlTabFactory.cs
Normal 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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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." ),
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue