mirror of
https://github.com/xivdev/Penumbra.git
synced 2026-02-23 08:17:59 +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 AlwaysOpenDefaultImport { get; set; } = false;
|
||||||
public bool KeepDefaultMetaChanges { get; set; } = false;
|
public bool KeepDefaultMetaChanges { get; set; } = false;
|
||||||
public string DefaultModAuthor { get; set; } = DefaultTexToolsData.Author;
|
public string DefaultModAuthor { get; set; } = DefaultTexToolsData.Author;
|
||||||
|
public bool EditRawTileTransforms { get; set; } = false;
|
||||||
|
|
||||||
public Dictionary<ColorId, uint> Colors { get; set; }
|
public Dictionary<ColorId, uint> Colors { get; set; }
|
||||||
= Enum.GetValues<ColorId>().ToDictionary(c => c, c => c.Data().DefaultColor);
|
= Enum.GetValues<ColorId>().ToDictionary(c => c, c => c.Data().DefaultColor);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
using Penumbra.GameData.Interop;
|
using Penumbra.GameData.Interop;
|
||||||
using Penumbra.Interop.SafeHandles;
|
using Penumbra.Interop.SafeHandles;
|
||||||
|
|
||||||
|
|
@ -7,10 +8,6 @@ namespace Penumbra.Interop.MaterialPreview;
|
||||||
|
|
||||||
public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase
|
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 IFramework _framework;
|
||||||
|
|
||||||
private readonly Texture** _colorTableTexture;
|
private readonly Texture** _colorTableTexture;
|
||||||
|
|
@ -18,6 +15,9 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase
|
||||||
|
|
||||||
private bool _updatePending;
|
private bool _updatePending;
|
||||||
|
|
||||||
|
public int Width { get; }
|
||||||
|
public int Height { get; }
|
||||||
|
|
||||||
public Half[] ColorTable { get; }
|
public Half[] ColorTable { get; }
|
||||||
|
|
||||||
public LiveColorTablePreviewer(ObjectManager objects, IFramework framework, MaterialInfo materialInfo)
|
public LiveColorTablePreviewer(ObjectManager objects, IFramework framework, MaterialInfo materialInfo)
|
||||||
|
|
@ -33,18 +33,24 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase
|
||||||
if (colorSetTextures == null)
|
if (colorSetTextures == null)
|
||||||
throw new InvalidOperationException("Draw object doesn't have color table textures");
|
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);
|
_originalColorTableTexture = new SafeTextureHandle(*_colorTableTexture, true);
|
||||||
if (_originalColorTableTexture == null)
|
if (_originalColorTableTexture == null)
|
||||||
throw new InvalidOperationException("Material doesn't have a color table");
|
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;
|
_updatePending = true;
|
||||||
|
|
||||||
framework.Update += OnFrameworkUpdate;
|
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)
|
protected override void Clear(bool disposing, bool reset)
|
||||||
{
|
{
|
||||||
_framework.Update -= OnFrameworkUpdate;
|
_framework.Update -= OnFrameworkUpdate;
|
||||||
|
|
@ -74,8 +80,8 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var textureSize = stackalloc int[2];
|
var textureSize = stackalloc int[2];
|
||||||
textureSize[0] = TextureWidth;
|
textureSize[0] = Width;
|
||||||
textureSize[1] = TextureHeight;
|
textureSize[1] = Height;
|
||||||
|
|
||||||
using var texture =
|
using var texture =
|
||||||
new SafeTextureHandle(Device.Instance()->CreateTexture2D(textureSize, 1, 0x2460, 0x80000804, 7), false);
|
new SafeTextureHandle(Device.Instance()->CreateTexture2D(textureSize, 1, 0x2460, 0x80000804, 7), false);
|
||||||
|
|
@ -104,6 +110,6 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase
|
||||||
if (colorSetTextures == null)
|
if (colorSetTextures == null)
|
||||||
return false;
|
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 ShaderPackage* _shaderPackage;
|
||||||
|
|
||||||
private readonly uint _originalShPkFlags;
|
private readonly uint _originalShPkFlags;
|
||||||
private readonly float[] _originalMaterialParameter;
|
private readonly byte[] _originalMaterialParameter;
|
||||||
private readonly uint[] _originalSamplerFlags;
|
private readonly uint[] _originalSamplerFlags;
|
||||||
|
|
||||||
public LiveMaterialPreviewer(ObjectManager objects, MaterialInfo materialInfo)
|
public LiveMaterialPreviewer(ObjectManager objects, MaterialInfo materialInfo)
|
||||||
: base(objects, materialInfo)
|
: base(objects, materialInfo)
|
||||||
|
|
@ -28,7 +28,7 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase
|
||||||
|
|
||||||
_originalShPkFlags = Material->ShaderFlags;
|
_originalShPkFlags = Material->ShaderFlags;
|
||||||
|
|
||||||
_originalMaterialParameter = Material->MaterialParameterCBuffer->TryGetBuffer().ToArray();
|
_originalMaterialParameter = Material->MaterialParameterCBuffer->TryGetBuffer<byte>().ToArray();
|
||||||
|
|
||||||
_originalSamplerFlags = new uint[Material->TextureCount];
|
_originalSamplerFlags = new uint[Material->TextureCount];
|
||||||
for (var i = 0; i < _originalSamplerFlags.Length; ++i)
|
for (var i = 0; i < _originalSamplerFlags.Length; ++i)
|
||||||
|
|
@ -43,7 +43,7 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Material->ShaderFlags = _originalShPkFlags;
|
Material->ShaderFlags = _originalShPkFlags;
|
||||||
var materialParameter = Material->MaterialParameterCBuffer->TryGetBuffer();
|
var materialParameter = Material->MaterialParameterCBuffer->TryGetBuffer<byte>();
|
||||||
if (!materialParameter.IsEmpty)
|
if (!materialParameter.IsEmpty)
|
||||||
_originalMaterialParameter.AsSpan().CopyTo(materialParameter);
|
_originalMaterialParameter.AsSpan().CopyTo(materialParameter);
|
||||||
|
|
||||||
|
|
@ -59,7 +59,7 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase
|
||||||
Material->ShaderFlags = shPkFlags;
|
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())
|
if (!CheckValidity())
|
||||||
return;
|
return;
|
||||||
|
|
@ -68,7 +68,7 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase
|
||||||
if (constantBuffer == null)
|
if (constantBuffer == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var buffer = constantBuffer->TryGetBuffer();
|
var buffer = constantBuffer->TryGetBuffer<byte>();
|
||||||
if (buffer.IsEmpty)
|
if (buffer.IsEmpty)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
@ -78,12 +78,10 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase
|
||||||
if (parameter.CRC != parameterCrc)
|
if (parameter.CRC != parameterCrc)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if ((parameter.Offset & 0x3) != 0
|
if (parameter.Offset + parameter.Size > buffer.Length)
|
||||||
|| (parameter.Size & 0x3) != 0
|
|
||||||
|| (parameter.Offset + parameter.Size) >> 2 > buffer.Length)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
value.TryCopyTo(buffer.Slice(parameter.Offset >> 2, parameter.Size >> 2)[offset..]);
|
value.TryCopyTo(buffer.Slice(parameter.Offset, parameter.Size)[offset..]);
|
||||||
return;
|
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 ImGuiNET;
|
||||||
using OtterGui;
|
using OtterGui;
|
||||||
using OtterGui.Raii;
|
using OtterGui.Raii;
|
||||||
using OtterGui.Text;
|
using Penumbra.UI.AdvancedWindow.Materials;
|
||||||
using OtterGui.Widgets;
|
|
||||||
using Penumbra.GameData.Files;
|
|
||||||
using Penumbra.String.Classes;
|
|
||||||
using Penumbra.UI.Classes;
|
|
||||||
|
|
||||||
namespace Penumbra.UI.AdvancedWindow;
|
namespace Penumbra.UI.AdvancedWindow;
|
||||||
|
|
||||||
|
|
@ -17,177 +13,10 @@ public partial class ModEditWindow
|
||||||
|
|
||||||
private bool DrawMaterialPanel(MtrlTab tab, bool disabled)
|
private bool DrawMaterialPanel(MtrlTab tab, bool disabled)
|
||||||
{
|
{
|
||||||
DrawVersionUpdate(tab, disabled);
|
if (tab.DrawVersionUpdate(disabled))
|
||||||
DrawMaterialLivePreviewRebind(tab, disabled);
|
_materialTab.SaveFile();
|
||||||
|
|
||||||
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
|
return tab.DrawPanel(disabled);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawMaterialReassignmentTab()
|
private void DrawMaterialReassignmentTab()
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ namespace Penumbra.UI.AdvancedWindow;
|
||||||
|
|
||||||
public partial class ModEditWindow
|
public partial class ModEditWindow
|
||||||
{
|
{
|
||||||
|
private readonly FileDialogService _fileDialog;
|
||||||
private readonly ResourceTreeFactory _resourceTreeFactory;
|
private readonly ResourceTreeFactory _resourceTreeFactory;
|
||||||
private readonly ResourceTreeViewer _quickImportViewer;
|
private readonly ResourceTreeViewer _quickImportViewer;
|
||||||
private readonly Dictionary<FullPath, IWritable?> _quickImportWritables = new();
|
private readonly Dictionary<FullPath, IWritable?> _quickImportWritables = new();
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,8 @@ using Penumbra.Collections.Manager;
|
||||||
using Penumbra.Communication;
|
using Penumbra.Communication;
|
||||||
using Penumbra.GameData.Enums;
|
using Penumbra.GameData.Enums;
|
||||||
using Penumbra.GameData.Files;
|
using Penumbra.GameData.Files;
|
||||||
using Penumbra.GameData.Interop;
|
|
||||||
using Penumbra.Import.Models;
|
using Penumbra.Import.Models;
|
||||||
using Penumbra.Import.Textures;
|
using Penumbra.Import.Textures;
|
||||||
using Penumbra.Interop.Hooks.Objects;
|
|
||||||
using Penumbra.Interop.ResourceTree;
|
using Penumbra.Interop.ResourceTree;
|
||||||
using Penumbra.Meta;
|
using Penumbra.Meta;
|
||||||
using Penumbra.Mods;
|
using Penumbra.Mods;
|
||||||
|
|
@ -26,6 +24,7 @@ using Penumbra.Mods.SubMods;
|
||||||
using Penumbra.Services;
|
using Penumbra.Services;
|
||||||
using Penumbra.String;
|
using Penumbra.String;
|
||||||
using Penumbra.String.Classes;
|
using Penumbra.String.Classes;
|
||||||
|
using Penumbra.UI.AdvancedWindow.Materials;
|
||||||
using Penumbra.UI.AdvancedWindow.Meta;
|
using Penumbra.UI.AdvancedWindow.Meta;
|
||||||
using Penumbra.UI.Classes;
|
using Penumbra.UI.Classes;
|
||||||
using Penumbra.Util;
|
using Penumbra.Util;
|
||||||
|
|
@ -39,20 +38,17 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
|
||||||
|
|
||||||
public readonly MigrationManager MigrationManager;
|
public readonly MigrationManager MigrationManager;
|
||||||
|
|
||||||
private readonly PerformanceTracker _performance;
|
private readonly PerformanceTracker _performance;
|
||||||
private readonly ModEditor _editor;
|
private readonly ModEditor _editor;
|
||||||
private readonly Configuration _config;
|
private readonly Configuration _config;
|
||||||
private readonly ItemSwapTab _itemSwapTab;
|
private readonly ItemSwapTab _itemSwapTab;
|
||||||
private readonly MetaFileManager _metaFileManager;
|
private readonly MetaFileManager _metaFileManager;
|
||||||
private readonly ActiveCollections _activeCollections;
|
private readonly ActiveCollections _activeCollections;
|
||||||
private readonly StainService _stainService;
|
private readonly ModMergeTab _modMergeTab;
|
||||||
private readonly ModMergeTab _modMergeTab;
|
private readonly CommunicatorService _communicator;
|
||||||
private readonly CommunicatorService _communicator;
|
private readonly IDragDropManager _dragDropManager;
|
||||||
private readonly IDragDropManager _dragDropManager;
|
private readonly IDataManager _gameData;
|
||||||
private readonly IDataManager _gameData;
|
private readonly IFramework _framework;
|
||||||
private readonly IFramework _framework;
|
|
||||||
private readonly ObjectManager _objects;
|
|
||||||
private readonly CharacterBaseDestructor _characterBaseDestructor;
|
|
||||||
|
|
||||||
private Vector2 _iconSize = Vector2.Zero;
|
private Vector2 _iconSize = Vector2.Zero;
|
||||||
private bool _allowReduplicate;
|
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 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.
|
/// If no redirection is found in either of those options, returns the original path.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
private FullPath FindBestMatch(Utf8GamePath path)
|
internal FullPath FindBestMatch(Utf8GamePath path)
|
||||||
{
|
{
|
||||||
var currentFile = _activeCollections.Current.ResolvePath(path);
|
var currentFile = _activeCollections.Current.ResolvePath(path);
|
||||||
if (currentFile != null)
|
if (currentFile != null)
|
||||||
|
|
@ -562,7 +558,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
|
||||||
return new FullPath(path);
|
return new FullPath(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
private HashSet<Utf8GamePath> FindPathsStartingWith(CiByteString prefix)
|
internal HashSet<Utf8GamePath> FindPathsStartingWith(CiByteString prefix)
|
||||||
{
|
{
|
||||||
var ret = new HashSet<Utf8GamePath>();
|
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,
|
public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, IDataManager gameData,
|
||||||
Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager,
|
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,
|
CommunicatorService communicator, TextureManager textures, ModelManager models, IDragDropManager dragDropManager,
|
||||||
ResourceTreeViewerFactory resourceTreeViewerFactory, ObjectManager objects, IFramework framework,
|
ResourceTreeViewerFactory resourceTreeViewerFactory, IFramework framework,
|
||||||
CharacterBaseDestructor characterBaseDestructor, MetaDrawers metaDrawers, MigrationManager migrationManager)
|
MetaDrawers metaDrawers, MigrationManager migrationManager,
|
||||||
|
MtrlTabFactory mtrlTabFactory)
|
||||||
: base(WindowBaseLabel)
|
: base(WindowBaseLabel)
|
||||||
{
|
{
|
||||||
_performance = performance;
|
_performance = performance;
|
||||||
_itemSwapTab = itemSwapTab;
|
_itemSwapTab = itemSwapTab;
|
||||||
_gameData = gameData;
|
_gameData = gameData;
|
||||||
_config = config;
|
_config = config;
|
||||||
_editor = editor;
|
_editor = editor;
|
||||||
_metaFileManager = metaFileManager;
|
_metaFileManager = metaFileManager;
|
||||||
_stainService = stainService;
|
_activeCollections = activeCollections;
|
||||||
_activeCollections = activeCollections;
|
_modMergeTab = modMergeTab;
|
||||||
_modMergeTab = modMergeTab;
|
_communicator = communicator;
|
||||||
_communicator = communicator;
|
_dragDropManager = dragDropManager;
|
||||||
_dragDropManager = dragDropManager;
|
_textures = textures;
|
||||||
_textures = textures;
|
_models = models;
|
||||||
_models = models;
|
_fileDialog = fileDialog;
|
||||||
_fileDialog = fileDialog;
|
_framework = framework;
|
||||||
_objects = objects;
|
MigrationManager = migrationManager;
|
||||||
_framework = framework;
|
_metaDrawers = metaDrawers;
|
||||||
_characterBaseDestructor = characterBaseDestructor;
|
|
||||||
MigrationManager = migrationManager;
|
|
||||||
_metaDrawers = metaDrawers;
|
|
||||||
_materialTab = new FileEditor<MtrlTab>(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl",
|
_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,
|
() => 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",
|
_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,
|
() => PopulateIsOnPlayer(_editor.Files.Mdl, ResourceType.Mdl), DrawModelPanel, () => Mod?.ModPath.FullName ?? string.Empty,
|
||||||
(bytes, path, _) => new MdlTab(this, bytes, path));
|
(bytes, path, _) => new MdlTab(this, bytes, path));
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ public enum ColorId
|
||||||
NoAssignment,
|
NoAssignment,
|
||||||
SelectorPriority,
|
SelectorPriority,
|
||||||
InGameHighlight,
|
InGameHighlight,
|
||||||
|
InGameHighlight2,
|
||||||
ResTreeLocalPlayer,
|
ResTreeLocalPlayer,
|
||||||
ResTreePlayer,
|
ResTreePlayer,
|
||||||
ResTreeNetworked,
|
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.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.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.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.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.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." ),
|
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();
|
UiHelpers.DefaultLineSpace();
|
||||||
|
|
||||||
DrawModHandlingSettings();
|
DrawModHandlingSettings();
|
||||||
|
UiHelpers.DefaultLineSpace();
|
||||||
|
|
||||||
|
DrawModEditorSettings();
|
||||||
ImGui.NewLine();
|
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.");
|
"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
|
#endregion
|
||||||
|
|
||||||
/// <summary> Draw the entire Color subsection. </summary>
|
/// <summary> Draw the entire Color subsection. </summary>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue