Merge branch 'dtme'

This commit is contained in:
Ottermandias 2024-08-04 22:48:41 +02:00
commit f2094c2c58
38 changed files with 4540 additions and 2496 deletions

View file

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

View file

@ -49,7 +49,7 @@ public class MaterialExporter
private static MaterialBuilder BuildCharacter(Material material, string name)
{
// Build the textures from the color table.
var table = new LegacyColorTable(material.Mtrl.Table);
var table = new LegacyColorTable(material.Mtrl.Table!);
var normal = material.Textures[TextureUsage.SamplerNormal];
@ -103,6 +103,7 @@ public class MaterialExporter
// TODO: It feels a little silly to request the entire normal here when extracting the normal only needs some of the components.
// As a future refactor, it would be neat to accept a single-channel field here, and then do composition of other stuff later.
// TODO(Dawntrail): Use the dedicated index (_id) map, that is not embedded in the normal map's alpha channel anymore.
private readonly struct ProcessCharacterNormalOperation(Image<Rgba32> normal, LegacyColorTable table) : IRowOperation
{
public Image<Rgba32> Normal { get; } = normal.Clone();
@ -139,17 +140,17 @@ public class MaterialExporter
var nextRow = table[tableRow.Next];
// Base colour (table, .b)
var lerpedDiffuse = Vector3.Lerp(prevRow.Diffuse, nextRow.Diffuse, tableRow.Weight);
var lerpedDiffuse = Vector3.Lerp((Vector3)prevRow.DiffuseColor, (Vector3)nextRow.DiffuseColor, tableRow.Weight);
baseColorSpan[x].FromVector4(new Vector4(lerpedDiffuse, 1));
baseColorSpan[x].A = normalPixel.B;
// Specular (table)
var lerpedSpecularColor = Vector3.Lerp(prevRow.Specular, nextRow.Specular, tableRow.Weight);
var lerpedSpecularFactor = float.Lerp(prevRow.SpecularStrength, nextRow.SpecularStrength, tableRow.Weight);
var lerpedSpecularColor = Vector3.Lerp((Vector3)prevRow.SpecularColor, (Vector3)nextRow.SpecularColor, tableRow.Weight);
var lerpedSpecularFactor = float.Lerp((float)prevRow.SpecularMask, (float)nextRow.SpecularMask, tableRow.Weight);
specularSpan[x].FromVector4(new Vector4(lerpedSpecularColor, lerpedSpecularFactor));
// Emissive (table)
var lerpedEmissive = Vector3.Lerp(prevRow.Emissive, nextRow.Emissive, tableRow.Weight);
var lerpedEmissive = Vector3.Lerp((Vector3)prevRow.EmissiveColor, (Vector3)nextRow.EmissiveColor, tableRow.Weight);
emissiveSpan[x].FromVector4(new Vector4(lerpedEmissive, 1));
// Normal (.rg)

View file

@ -18,9 +18,7 @@ public static class TextureDrawer
{
if (texture.TextureWrap != null)
{
size = size.X < texture.TextureWrap.Width
? size with { Y = texture.TextureWrap.Height * size.X / texture.TextureWrap.Width }
: new Vector2(texture.TextureWrap.Width, texture.TextureWrap.Height);
size = texture.TextureWrap.Size.Contain(size);
ImGui.Image(texture.TextureWrap.ImGuiHandle, size);
DrawData(texture);

View file

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

View file

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

View file

@ -233,8 +233,8 @@ internal unsafe partial record ResolveContext(
node.Children.Add(shpkNode);
}
var shpkFile = Global.WithUiData && shpkNode != null ? Global.TreeBuildCache.ReadShaderPackage(shpkNode.FullPath) : null;
var shpk = Global.WithUiData && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null;
var shpkNames = Global.WithUiData && shpkNode != null ? Global.TreeBuildCache.ReadShaderPackageNames(shpkNode.FullPath) : null;
var shpk = Global.WithUiData && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null;
var alreadyProcessedSamplerIds = new HashSet<uint>();
for (var i = 0; i < resource->TextureCount; i++)
@ -258,7 +258,12 @@ internal unsafe partial record ResolveContext(
alreadyProcessedSamplerIds.Add(samplerId.Value);
var samplerCrc = GetSamplerCrcById(shpk, samplerId.Value);
if (samplerCrc.HasValue)
name = shpkFile?.GetSamplerById(samplerCrc.Value)?.Name ?? $"Texture 0x{samplerCrc.Value:X8}";
{
if (shpkNames != null && shpkNames.TryGetValue(samplerCrc.Value, out var samplerName))
name = samplerName.Value;
else
name = $"Texture 0x{samplerCrc.Value:X8}";
}
}
}

View file

@ -1,8 +1,11 @@
using System.IO.MemoryMappedFiles;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Files;
using Penumbra.GameData.Files.ShaderStructs;
using Penumbra.GameData.Files.Utility;
using Penumbra.GameData.Interop;
using Penumbra.GameData.Structs;
using Penumbra.String.Classes;
@ -11,7 +14,7 @@ namespace Penumbra.Interop.ResourceTree;
internal readonly struct TreeBuildCache(ObjectManager objects, IDataManager dataManager, ActorManager actors)
{
private readonly Dictionary<FullPath, ShpkFile?> _shaderPackages = [];
private readonly Dictionary<FullPath, IReadOnlyDictionary<uint, Name>?> _shaderPackageNames = [];
public unsafe bool IsLocalPlayerRelated(ICharacter character)
{
@ -68,10 +71,10 @@ internal readonly struct TreeBuildCache(ObjectManager objects, IDataManager data
}
/// <summary> Try to read a shpk file from the given path and cache it on success. </summary>
public ShpkFile? ReadShaderPackage(FullPath path)
=> ReadFile(dataManager, path, _shaderPackages, bytes => new ShpkFile(bytes));
public IReadOnlyDictionary<uint, Name>? ReadShaderPackageNames(FullPath path)
=> ReadFile(dataManager, path, _shaderPackageNames, bytes => ShpkFile.FastExtractNames(bytes.Span));
private static T? ReadFile<T>(IDataManager dataManager, FullPath path, Dictionary<FullPath, T?> cache, Func<byte[], T> parseFile)
private static T? ReadFile<T>(IDataManager dataManager, FullPath path, Dictionary<FullPath, T?> cache, Func<ReadOnlyMemory<byte>, T> parseFile)
where T : class
{
if (path.FullName.Length == 0)
@ -86,7 +89,8 @@ internal readonly struct TreeBuildCache(ObjectManager objects, IDataManager data
{
if (path.IsRooted)
{
parsed = parseFile(File.ReadAllBytes(pathStr));
using var mmFile = MmioMemoryManager.CreateFromFile(pathStr, access: MemoryMappedFileAccess.Read);
parsed = parseFile(mmFile.Memory);
}
else
{

View file

@ -0,0 +1,119 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using OtterGui.Services;
using SharpDX.Direct3D;
using SharpDX.Direct3D11;
namespace Penumbra.Interop.Services;
/// <summary>
/// Creates ImGui handles over slices of array textures, and manages their lifetime.
/// </summary>
public sealed unsafe class TextureArraySlicer : IUiService, IDisposable
{
private const uint InitialTimeToLive = 2;
private readonly Dictionary<(nint XivTexture, byte SliceIndex), SliceState> _activeSlices = [];
private readonly HashSet<(nint XivTexture, byte SliceIndex)> _expiredKeys = [];
/// <remarks> Caching this across frames will cause a crash to desktop. </remarks>
public nint GetImGuiHandle(Texture* texture, byte sliceIndex)
{
if (texture == null)
throw new ArgumentNullException(nameof(texture));
if (sliceIndex >= texture->ArraySize)
throw new ArgumentOutOfRangeException(nameof(sliceIndex), $"Slice index ({sliceIndex}) is greater than or equal to the texture array size ({texture->ArraySize})");
if (_activeSlices.TryGetValue(((nint)texture, sliceIndex), out var state))
{
state.Refresh();
return (nint)state.ShaderResourceView;
}
var srv = (ShaderResourceView)(nint)texture->D3D11ShaderResourceView;
var description = srv.Description;
switch (description.Dimension)
{
case ShaderResourceViewDimension.Texture1D:
case ShaderResourceViewDimension.Texture2D:
case ShaderResourceViewDimension.Texture2DMultisampled:
case ShaderResourceViewDimension.Texture3D:
case ShaderResourceViewDimension.TextureCube:
// This function treats these as single-slice arrays.
// As per the range check above, the only valid slice (i. e. 0) has been requested, therefore there is nothing to do.
break;
case ShaderResourceViewDimension.Texture1DArray:
description.Texture1DArray.FirstArraySlice = sliceIndex;
description.Texture2DArray.ArraySize = 1;
break;
case ShaderResourceViewDimension.Texture2DArray:
description.Texture2DArray.FirstArraySlice = sliceIndex;
description.Texture2DArray.ArraySize = 1;
break;
case ShaderResourceViewDimension.Texture2DMultisampledArray:
description.Texture2DMSArray.FirstArraySlice = sliceIndex;
description.Texture2DMSArray.ArraySize = 1;
break;
case ShaderResourceViewDimension.TextureCubeArray:
description.TextureCubeArray.First2DArrayFace = sliceIndex * 6;
description.TextureCubeArray.CubeCount = 1;
break;
default:
throw new NotSupportedException($"{nameof(TextureArraySlicer)} does not support dimension {description.Dimension}");
}
state = new SliceState(new ShaderResourceView(srv.Device, srv.Resource, description));
_activeSlices.Add(((nint)texture, sliceIndex), state);
return (nint)state.ShaderResourceView;
}
public void Tick()
{
try
{
foreach (var (key, slice) in _activeSlices)
{
if (!slice.Tick())
_expiredKeys.Add(key);
}
foreach (var key in _expiredKeys)
{
_activeSlices.Remove(key);
}
}
finally
{
_expiredKeys.Clear();
}
}
public void Dispose()
{
foreach (var slice in _activeSlices.Values)
{
slice.Dispose();
}
}
private sealed class SliceState(ShaderResourceView shaderResourceView) : IDisposable
{
public readonly ShaderResourceView ShaderResourceView = shaderResourceView;
private uint _timeToLive = InitialTimeToLive;
public void Refresh()
{
_timeToLive = InitialTimeToLive;
}
public bool Tick()
{
if (unchecked(_timeToLive--) > 0)
return true;
ShaderResourceView.Dispose();
return false;
}
public void Dispose()
{
ShaderResourceView.Dispose();
}
}
}

View file

@ -5,10 +5,15 @@ namespace Penumbra.Interop.Structs;
[StructLayout(LayoutKind.Explicit)]
public unsafe struct CharacterUtilityData
{
public const int IndexHumanPbd = 63;
public const int IndexTransparentTex = 79;
public const int IndexDecalTex = 80;
public const int IndexSkinShpk = 83;
public const int IndexHumanPbd = 63;
public const int IndexTransparentTex = 79;
public const int IndexDecalTex = 80;
public const int IndexTileOrbArrayTex = 81;
public const int IndexTileNormArrayTex = 82;
public const int IndexSkinShpk = 83;
public const int IndexGudStm = 94;
public const int IndexLegacyStm = 95;
public const int IndexSphereDArrayTex = 96;
public static readonly MetaIndex[] EqdpIndices = Enum.GetNames<MetaIndex>()
.Zip(Enum.GetValues<MetaIndex>())
@ -97,8 +102,23 @@ public unsafe struct CharacterUtilityData
[FieldOffset(8 + IndexDecalTex * 8)]
public TextureResourceHandle* DecalTexResource;
[FieldOffset(8 + IndexTileOrbArrayTex * 8)]
public TextureResourceHandle* TileOrbArrayTexResource;
[FieldOffset(8 + IndexTileNormArrayTex * 8)]
public TextureResourceHandle* TileNormArrayTexResource;
[FieldOffset(8 + IndexSkinShpk * 8)]
public ResourceHandle* SkinShpkResource;
[FieldOffset(8 + IndexGudStm * 8)]
public ResourceHandle* GudStmResource;
[FieldOffset(8 + IndexLegacyStm * 8)]
public ResourceHandle* LegacyStmResource;
[FieldOffset(8 + IndexSphereDArrayTex * 8)]
public TextureResourceHandle* SphereDArrayTexResource;
// not included resources have no known use case.
}

View file

@ -72,6 +72,14 @@
<HintPath>$(DalamudLibPath)Iced.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="SharpDX">
<HintPath>$(DalamudLibPath)SharpDX.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="SharpDX.Direct3D11">
<HintPath>$(DalamudLibPath)SharpDX.Direct3D11.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="OtterTex.dll">
<HintPath>lib\OtterTex.dll</HintPath>
</Reference>

View file

@ -279,7 +279,7 @@ public class MigrationManager(Configuration config) : IService
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
using var f = File.Open(path, FileMode.Create, FileAccess.Write);
if (file.IsDawnTrail)
if (file.IsDawntrail)
{
file.MigrateToDawntrail();
Penumbra.Log.Debug($"Migrated material {reader.Entry.Key} to Dawntrail during import.");
@ -329,7 +329,7 @@ public class MigrationManager(Configuration config) : IService
try
{
var mtrl = new MtrlFile(data);
if (mtrl.IsDawnTrail)
if (mtrl.IsDawntrail)
return data;
mtrl.MigrateToDawntrail();

View file

@ -6,19 +6,25 @@ using OtterGui.Services;
using OtterGui.Widgets;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Files;
using Penumbra.UI.AdvancedWindow;
using Penumbra.GameData.Files.StainMapStructs;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.UI.AdvancedWindow.Materials;
namespace Penumbra.Services;
public class StainService : IService
{
public sealed class StainTemplateCombo(FilterComboColors stainCombo, StmFile stmFile)
: FilterComboCache<ushort>(stmFile.Entries.Keys.Prepend((ushort)0), MouseWheelType.None, Penumbra.Log)
public sealed class StainTemplateCombo<TDyePack>(FilterComboColors[] stainCombos, StmFile<TDyePack> stmFile)
: FilterComboCache<ushort>(stmFile.Entries.Keys.Prepend((ushort)0), MouseWheelType.None, Penumbra.Log) where TDyePack : unmanaged, IDyePack
{
// FIXME There might be a better way to handle that.
public int CurrentDyeChannel = 0;
protected override float GetFilterWidth()
{
var baseSize = ImGui.CalcTextSize("0000").X + ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().ItemInnerSpacing.X;
if (stainCombo.CurrentSelection.Key == 0)
if (stainCombos[CurrentDyeChannel].CurrentSelection.Key == 0)
return baseSize;
return baseSize + ImGui.GetTextLineHeight() * 3 + ImGui.GetStyle().ItemInnerSpacing.X * 3;
@ -47,33 +53,73 @@ public class StainService : IService
protected override bool DrawSelectable(int globalIdx, bool selected)
{
var ret = base.DrawSelectable(globalIdx, selected);
var selection = stainCombo.CurrentSelection.Key;
var selection = stainCombos[CurrentDyeChannel].CurrentSelection.Key;
if (selection == 0 || !stmFile.TryGetValue(Items[globalIdx], selection, out var colors))
return ret;
ImGui.SameLine();
var frame = new Vector2(ImGui.GetTextLineHeight());
ImGui.ColorButton("D", new Vector4(ModEditWindow.PseudoSqrtRgb(colors.Diffuse), 1), 0, frame);
ImGui.ColorButton("D", new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.DiffuseColor), 1), 0, frame);
ImGui.SameLine();
ImGui.ColorButton("S", new Vector4(ModEditWindow.PseudoSqrtRgb(colors.Specular), 1), 0, frame);
ImGui.ColorButton("S", new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.SpecularColor), 1), 0, frame);
ImGui.SameLine();
ImGui.ColorButton("E", new Vector4(ModEditWindow.PseudoSqrtRgb(colors.Emissive), 1), 0, frame);
ImGui.ColorButton("E", new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.EmissiveColor), 1), 0, frame);
return ret;
}
}
public readonly DictStain StainData;
public readonly FilterComboColors StainCombo;
public readonly StmFile StmFile;
public readonly StainTemplateCombo TemplateCombo;
public const int ChannelCount = 2;
public StainService(IDataManager dataManager, DictStain stainData)
public readonly DictStain StainData;
public readonly FilterComboColors StainCombo1;
public readonly FilterComboColors StainCombo2; // FIXME is there a better way to handle this?
public readonly StmFile<LegacyDyePack> LegacyStmFile;
public readonly StmFile<DyePack> GudStmFile;
public readonly StainTemplateCombo<LegacyDyePack> LegacyTemplateCombo;
public readonly StainTemplateCombo<DyePack> GudTemplateCombo;
public unsafe StainService(IDataManager dataManager, CharacterUtility characterUtility, DictStain stainData)
{
StainData = stainData;
StainCombo = new FilterComboColors(140, MouseWheelType.None,
() => StainData.Value.Prepend(new KeyValuePair<byte, (string Name, uint Dye, bool Gloss)>(0, ("None", 0, false))).ToList(),
Penumbra.Log);
StmFile = new StmFile(dataManager);
TemplateCombo = new StainTemplateCombo(StainCombo, StmFile);
StainData = stainData;
StainCombo1 = CreateStainCombo();
StainCombo2 = CreateStainCombo();
LegacyStmFile = LoadStmFile<LegacyDyePack>(characterUtility.Address->LegacyStmResource, dataManager);
GudStmFile = LoadStmFile<DyePack>(characterUtility.Address->GudStmResource, dataManager);
FilterComboColors[] stainCombos = [StainCombo1, StainCombo2];
LegacyTemplateCombo = new StainTemplateCombo<LegacyDyePack>(stainCombos, LegacyStmFile);
GudTemplateCombo = new StainTemplateCombo<DyePack>(stainCombos, GudStmFile);
}
/// <summary> Retrieves the <see cref="FilterComboColors"/> instance for the given channel. Indexing is zero-based. </summary>
public FilterComboColors GetStainCombo(int channel)
=> channel switch
{
0 => StainCombo1,
1 => StainCombo2,
_ => throw new ArgumentOutOfRangeException(nameof(channel), channel, $"Unsupported dye channel {channel} (supported values are 0 and 1)")
};
/// <summary> Loads a STM file. Opportunistically attempts to re-use the file already read by the game, with Lumina fallback. </summary>
private static unsafe StmFile<TDyePack> LoadStmFile<TDyePack>(ResourceHandle* stmResourceHandle, IDataManager dataManager) where TDyePack : unmanaged, IDyePack
{
if (stmResourceHandle != null)
{
var stmData = stmResourceHandle->CsHandle.GetDataSpan();
if (stmData.Length > 0)
{
Penumbra.Log.Debug($"[StainService] Loading StmFile<{typeof(TDyePack)}> from ResourceHandle 0x{(nint)stmResourceHandle:X}");
return new StmFile<TDyePack>(stmData);
}
}
Penumbra.Log.Debug($"[StainService] Loading StmFile<{typeof(TDyePack)}> from Lumina");
return new StmFile<TDyePack>(dataManager);
}
private FilterComboColors CreateStainCombo()
=> new(140, MouseWheelType.None,
() => StainData.Value.Prepend(new KeyValuePair<byte, (string Name, uint Dye, bool Gloss)>(0, ("None", 0, false))).ToList(),
Penumbra.Log);
}

View file

@ -0,0 +1,71 @@
using System.Collections.Frozen;
using OtterGui.Text.Widget.Editors;
using Penumbra.GameData.Files.ShaderStructs;
namespace Penumbra.UI.AdvancedWindow.Materials;
public static class ConstantEditors
{
public static readonly IEditor<byte> DefaultFloat = Editors.DefaultFloat.AsByteEditor();
public static readonly IEditor<byte> DefaultInt = Editors.DefaultInt.AsByteEditor();
public static readonly IEditor<byte> DefaultIntAsFloat = Editors.DefaultInt.IntAsFloatEditor().AsByteEditor();
public static readonly IEditor<byte> DefaultColor = ColorEditor.HighDynamicRange.Reinterpreting<byte>();
/// <summary>
/// Material constants known to be encoded as native <see cref="int"/>s.
///
/// A <see cref="float"/> editor is nonfunctional for them, as typical values for these constants would fall into the IEEE 754 denormalized number range.
/// </summary>
private static readonly FrozenSet<Name> KnownIntConstants;
static ConstantEditors()
{
IReadOnlyList<Name> knownIntConstants = [
"g_ToonIndex",
"g_ToonSpecIndex",
];
KnownIntConstants = knownIntConstants.ToFrozenSet();
}
public static IEditor<byte> DefaultFor(Name name, MaterialTemplatePickers? materialTemplatePickers = null)
{
if (materialTemplatePickers != null)
{
if (name == Names.SphereMapIndexConstantName)
return materialTemplatePickers.SphereMapIndexPicker;
else if (name == Names.TileIndexConstantName)
return materialTemplatePickers.TileIndexPicker;
}
if (name.Value != null && name.Value.EndsWith("Color"))
return DefaultColor;
if (KnownIntConstants.Contains(name))
return DefaultInt;
return DefaultFloat;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IEditor<byte> AsByteEditor<T>(this IEditor<T> inner) where T : unmanaged
=> inner.Reinterpreting<byte>();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IEditor<float> IntAsFloatEditor(this IEditor<int> inner)
=> inner.Converting<float>(value => int.CreateSaturating(MathF.Round(value)), value => value);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IEditor<T> WithExponent<T>(this IEditor<T> inner, T exponent)
where T : unmanaged, IPowerFunctions<T>, IComparisonOperators<T, T, bool>
=> exponent == T.MultiplicativeIdentity
? inner
: inner.Converting(value => value < T.Zero ? -T.Pow(-value, T.MultiplicativeIdentity / exponent) : T.Pow(value, T.MultiplicativeIdentity / exponent), value => value < T.Zero ? -T.Pow(-value, exponent) : T.Pow(value, exponent));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IEditor<T> WithFactorAndBias<T>(this IEditor<T> inner, T factor, T bias)
where T : unmanaged, IMultiplicativeIdentity<T, T>, IAdditiveIdentity<T, T>, IMultiplyOperators<T, T, T>, IAdditionOperators<T, T, T>, ISubtractionOperators<T, T, T>, IDivisionOperators<T, T, T>, IEqualityOperators<T, T, bool>
=> factor == T.MultiplicativeIdentity && bias == T.AdditiveIdentity
? inner
: inner.Converting(value => (value - bias) / factor, value => value * factor + bias);
}

View file

@ -0,0 +1,177 @@
using Dalamud.Interface;
using FFXIVClientStructs.Interop;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Services;
using OtterGui.Text;
using OtterGui.Text.Widget.Editors;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
namespace Penumbra.UI.AdvancedWindow.Materials;
public sealed unsafe class MaterialTemplatePickers : IUiService
{
private const float MaximumTextureSize = 64.0f;
private readonly TextureArraySlicer _textureArraySlicer;
private readonly CharacterUtility _characterUtility;
public readonly IEditor<byte> TileIndexPicker;
public readonly IEditor<byte> SphereMapIndexPicker;
public MaterialTemplatePickers(TextureArraySlicer textureArraySlicer, CharacterUtility characterUtility)
{
_textureArraySlicer = textureArraySlicer;
_characterUtility = characterUtility;
TileIndexPicker = new Editor(DrawTileIndexPicker).AsByteEditor();
SphereMapIndexPicker = new Editor(DrawSphereMapIndexPicker).AsByteEditor();
}
public bool DrawTileIndexPicker(ReadOnlySpan<byte> label, ReadOnlySpan<byte> description, ref ushort value, bool compact)
=> _characterUtility.Address != null
&& DrawTextureArrayIndexPicker(label, description, ref value, compact, [
_characterUtility.Address->TileOrbArrayTexResource,
_characterUtility.Address->TileNormArrayTexResource,
]);
public bool DrawSphereMapIndexPicker(ReadOnlySpan<byte> label, ReadOnlySpan<byte> description, ref ushort value, bool compact)
=> _characterUtility.Address != null
&& DrawTextureArrayIndexPicker(label, description, ref value, compact, [
_characterUtility.Address->SphereDArrayTexResource,
]);
public bool DrawTextureArrayIndexPicker(ReadOnlySpan<byte> label, ReadOnlySpan<byte> description, ref ushort value, bool compact, ReadOnlySpan<Pointer<TextureResourceHandle>> textureRHs)
{
TextureResourceHandle* firstNonNullTextureRH = null;
foreach (var texture in textureRHs)
{
if (texture.Value != null && texture.Value->CsHandle.Texture != null)
{
firstNonNullTextureRH = texture;
break;
}
}
var firstNonNullTexture = firstNonNullTextureRH != null ? firstNonNullTextureRH->CsHandle.Texture : null;
var textureSize = firstNonNullTexture != null ? new Vector2(firstNonNullTexture->Width, firstNonNullTexture->Height).Contain(new Vector2(MaximumTextureSize)) : Vector2.Zero;
var count = firstNonNullTexture != null ? firstNonNullTexture->ArraySize : 0;
var ret = false;
var framePadding = ImGui.GetStyle().FramePadding;
var itemSpacing = ImGui.GetStyle().ItemSpacing;
using (var font = ImRaii.PushFont(UiBuilder.MonoFont))
{
var spaceSize = ImUtf8.CalcTextSize(" "u8).X;
var spaces = (int)((ImGui.CalcItemWidth() - framePadding.X * 2.0f - (compact ? 0.0f : (textureSize.X + itemSpacing.X) * textureRHs.Length)) / spaceSize);
using var padding = ImRaii.PushStyle(ImGuiStyleVar.FramePadding, framePadding + new Vector2(0.0f, Math.Max(textureSize.Y - ImGui.GetFrameHeight() + itemSpacing.Y, 0.0f) * 0.5f), !compact);
using var combo = ImUtf8.Combo(label, (value == ushort.MaxValue ? "-" : value.ToString()).PadLeft(spaces), ImGuiComboFlags.NoArrowButton | ImGuiComboFlags.HeightLarge);
if (combo.Success && firstNonNullTextureRH != null)
{
var lineHeight = Math.Max(ImGui.GetTextLineHeightWithSpacing(), framePadding.Y * 2.0f + textureSize.Y);
var itemWidth = Math.Max(ImGui.GetContentRegionAvail().X, ImUtf8.CalcTextSize("MMM"u8).X + (itemSpacing.X + textureSize.X) * textureRHs.Length + framePadding.X * 2.0f);
using var center = ImRaii.PushStyle(ImGuiStyleVar.SelectableTextAlign, new Vector2(0, 0.5f));
using var clipper = ImUtf8.ListClipper(count, lineHeight);
while (clipper.Step())
{
for (var i = clipper.DisplayStart; i < clipper.DisplayEnd && i < count; i++)
{
if (ImUtf8.Selectable($"{i,3}", i == value, size: new(itemWidth, lineHeight)))
{
ret = value != i;
value = (ushort)i;
}
var rectMin = ImGui.GetItemRectMin();
var rectMax = ImGui.GetItemRectMax();
var textureRegionStart = new Vector2(
rectMax.X - framePadding.X - textureSize.X * textureRHs.Length - itemSpacing.X * (textureRHs.Length - 1),
rectMin.Y + framePadding.Y);
var maxSize = new Vector2(textureSize.X, rectMax.Y - framePadding.Y - textureRegionStart.Y);
DrawTextureSlices(textureRegionStart, maxSize, itemSpacing.X, textureRHs, (byte)i);
}
}
}
}
if (!compact && value != ushort.MaxValue)
{
var cbRectMin = ImGui.GetItemRectMin();
var cbRectMax = ImGui.GetItemRectMax();
var cbTextureRegionStart = new Vector2(cbRectMax.X - framePadding.X - textureSize.X * textureRHs.Length - itemSpacing.X * (textureRHs.Length - 1), cbRectMin.Y + framePadding.Y);
var cbMaxSize = new Vector2(textureSize.X, cbRectMax.Y - framePadding.Y - cbTextureRegionStart.Y);
DrawTextureSlices(cbTextureRegionStart, cbMaxSize, itemSpacing.X, textureRHs, (byte)value);
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled) && (description.Length > 0 || compact && value != ushort.MaxValue))
{
using var disabled = ImRaii.Enabled();
using var tt = ImUtf8.Tooltip();
if (description.Length > 0)
ImUtf8.Text(description);
if (compact && value != ushort.MaxValue)
{
ImGui.Dummy(new Vector2(textureSize.X * textureRHs.Length + itemSpacing.X * (textureRHs.Length - 1), textureSize.Y));
var rectMin = ImGui.GetItemRectMin();
var rectMax = ImGui.GetItemRectMax();
DrawTextureSlices(rectMin, textureSize, itemSpacing.X, textureRHs, (byte)value);
}
}
return ret;
}
public void DrawTextureSlices(Vector2 regionStart, Vector2 itemSize, float itemSpacing, ReadOnlySpan<Pointer<TextureResourceHandle>> textureRHs, byte sliceIndex)
{
for (var j = 0; j < textureRHs.Length; ++j)
{
if (textureRHs[j].Value == null)
continue;
var texture = textureRHs[j].Value->CsHandle.Texture;
if (texture == null)
continue;
var handle = _textureArraySlicer.GetImGuiHandle(texture, sliceIndex);
if (handle == 0)
continue;
var position = regionStart with { X = regionStart.X + (itemSize.X + itemSpacing) * j };
var size = new Vector2(texture->Width, texture->Height).Contain(itemSize);
position += (itemSize - size) * 0.5f;
ImGui.GetWindowDrawList().AddImage(handle, position, position + size, Vector2.Zero,
new Vector2(texture->Width / (float)texture->Width2, texture->Height / (float)texture->Height2));
}
}
private delegate bool DrawEditor(ReadOnlySpan<byte> label, ReadOnlySpan<byte> description, ref ushort value, bool compact);
private sealed class Editor(DrawEditor draw) : IEditor<float>
{
public bool Draw(Span<float> values, bool disabled)
{
var helper = Editors.PrepareMultiComponent(values.Length);
var ret = false;
for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx)
{
helper.SetupComponent(valueIdx);
var value = ushort.CreateSaturating(MathF.Round(values[valueIdx]));
if (disabled)
{
using var _ = ImRaii.Disabled();
draw(helper.Id, default, ref value, true);
}
else
{
if (draw(helper.Id, default, ref value, true))
{
values[valueIdx] = value;
ret = true;
}
}
}
return ret;
}
}
}

View file

@ -0,0 +1,624 @@
using Dalamud.Interface;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
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 ColorTableScalarSize = 65.0f;
private int _colorTableSelectedPair;
private bool DrawColorTable(ColorTable table, ColorDyeTable? dyeTable, bool disabled)
{
DrawColorTablePairSelector(table, disabled);
return DrawColorTablePairEditor(table, dyeTable, disabled);
}
private void DrawColorTablePairSelector(ColorTable table, bool disabled)
{
var style = ImGui.GetStyle();
var itemSpacing = style.ItemSpacing.X;
var itemInnerSpacing = style.ItemInnerSpacing.X;
var framePadding = style.FramePadding;
var buttonWidth = (ImGui.GetContentRegionAvail().X - itemSpacing * 7.0f) * 0.125f;
var frameHeight = ImGui.GetFrameHeight();
var highlighterSize = ImUtf8.CalcIconSize(FontAwesomeIcon.Crosshairs) + framePadding * 2.0f;
var spaceWidth = ImUtf8.CalcTextSize(" "u8).X;
var spacePadding = (int)MathF.Ceiling((highlighterSize.X + framePadding.X + itemInnerSpacing) / spaceWidth);
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
using var alignment = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f));
for (var i = 0; i < ColorTable.NumRows >> 1; i += 8)
{
for (var j = 0; j < 8; ++j)
{
var pairIndex = i + j;
using (ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.ButtonActive), pairIndex == _colorTableSelectedPair))
{
if (ImUtf8.Button($"#{pairIndex + 1}".PadLeft(3 + spacePadding),
new Vector2(buttonWidth, ImGui.GetFrameHeightWithSpacing() + frameHeight)))
_colorTableSelectedPair = pairIndex;
}
var rcMin = ImGui.GetItemRectMin() + framePadding;
var rcMax = ImGui.GetItemRectMax() - framePadding;
CtBlendRect(
rcMin with { X = rcMax.X - frameHeight * 3 - itemInnerSpacing * 2 },
rcMax with { X = rcMax.X - (frameHeight + itemInnerSpacing) * 2 },
ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[pairIndex << 1].DiffuseColor)),
ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[(pairIndex << 1) | 1].DiffuseColor))
);
CtBlendRect(
rcMin with { X = rcMax.X - frameHeight * 2 - itemInnerSpacing },
rcMax with { X = rcMax.X - frameHeight - itemInnerSpacing },
ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[pairIndex << 1].SpecularColor)),
ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[(pairIndex << 1) | 1].SpecularColor))
);
CtBlendRect(
rcMin with { X = rcMax.X - frameHeight }, rcMax,
ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[pairIndex << 1].EmissiveColor)),
ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[(pairIndex << 1) | 1].EmissiveColor))
);
if (j < 7)
ImGui.SameLine();
var cursor = ImGui.GetCursorScreenPos();
ImGui.SetCursorScreenPos(rcMin with { Y = float.Lerp(rcMin.Y, rcMax.Y, 0.5f) - highlighterSize.Y * 0.5f });
font.Pop();
ColorTableHighlightButton(pairIndex, disabled);
font.Push(UiBuilder.MonoFont);
ImGui.SetCursorScreenPos(cursor);
}
}
}
private bool DrawColorTablePairEditor(ColorTable table, ColorDyeTable? dyeTable, bool disabled)
{
var retA = false;
var retB = false;
var dyeA = dyeTable?[_colorTableSelectedPair << 1] ?? default;
var dyeB = dyeTable?[(_colorTableSelectedPair << 1) | 1] ?? default;
var previewDyeA = _stainService.GetStainCombo(dyeA.Channel).CurrentSelection.Key;
var previewDyeB = _stainService.GetStainCombo(dyeB.Channel).CurrentSelection.Key;
var dyePackA = _stainService.GudStmFile.GetValueOrNull(dyeA.Template, previewDyeA);
var dyePackB = _stainService.GudStmFile.GetValueOrNull(dyeB.Template, previewDyeB);
using (var columns = ImUtf8.Columns(2, "ColorTable"u8))
{
ColorTableCopyClipboardButton(_colorTableSelectedPair << 1);
ImUtf8.SameLineInner();
retA |= ColorTablePasteFromClipboardButton(_colorTableSelectedPair << 1, disabled);
ImGui.SameLine();
CenteredTextInRest($"Row {_colorTableSelectedPair + 1}A");
columns.Next();
ColorTableCopyClipboardButton((_colorTableSelectedPair << 1) | 1);
ImUtf8.SameLineInner();
retB |= ColorTablePasteFromClipboardButton((_colorTableSelectedPair << 1) | 1, disabled);
ImGui.SameLine();
CenteredTextInRest($"Row {_colorTableSelectedPair + 1}B");
}
DrawHeader(" Colors"u8);
using (var columns = ImUtf8.Columns(2, "ColorTable"u8))
{
using var dis = ImRaii.Disabled(disabled);
using (ImUtf8.PushId("ColorsA"u8))
{
retA |= DrawColors(table, dyeTable, dyePackA, _colorTableSelectedPair << 1);
}
columns.Next();
using (ImUtf8.PushId("ColorsB"u8))
{
retB |= DrawColors(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1);
}
}
DrawHeader(" Physical Parameters"u8);
using (var columns = ImUtf8.Columns(2, "ColorTable"u8))
{
using var dis = ImRaii.Disabled(disabled);
using (ImUtf8.PushId("PbrA"u8))
{
retA |= DrawPbr(table, dyeTable, dyePackA, _colorTableSelectedPair << 1);
}
columns.Next();
using (ImUtf8.PushId("PbrB"u8))
{
retB |= DrawPbr(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1);
}
}
DrawHeader(" Sheen Layer Parameters"u8);
using (var columns = ImUtf8.Columns(2, "ColorTable"u8))
{
using var dis = ImRaii.Disabled(disabled);
using (ImUtf8.PushId("SheenA"u8))
{
retA |= DrawSheen(table, dyeTable, dyePackA, _colorTableSelectedPair << 1);
}
columns.Next();
using (ImUtf8.PushId("SheenB"u8))
{
retB |= DrawSheen(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1);
}
}
DrawHeader(" Pair Blending"u8);
using (var columns = ImUtf8.Columns(2, "ColorTable"u8))
{
using var dis = ImRaii.Disabled(disabled);
using (ImUtf8.PushId("BlendingA"u8))
{
retA |= DrawBlending(table, dyeTable, dyePackA, _colorTableSelectedPair << 1);
}
columns.Next();
using (ImUtf8.PushId("BlendingB"u8))
{
retB |= DrawBlending(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1);
}
}
DrawHeader(" Material Template"u8);
using (var columns = ImUtf8.Columns(2, "ColorTable"u8))
{
using var dis = ImRaii.Disabled(disabled);
using (ImUtf8.PushId("TemplateA"u8))
{
retA |= DrawTemplate(table, dyeTable, dyePackA, _colorTableSelectedPair << 1);
}
columns.Next();
using (ImUtf8.PushId("TemplateB"u8))
{
retB |= DrawTemplate(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1);
}
}
if (dyeTable != null)
{
DrawHeader(" Dye Properties"u8);
using var columns = ImUtf8.Columns(2, "ColorTable"u8);
using var dis = ImRaii.Disabled(disabled);
using (ImUtf8.PushId("DyeA"u8))
{
retA |= DrawDye(dyeTable, dyePackA, _colorTableSelectedPair << 1);
}
columns.Next();
using (ImUtf8.PushId("DyeB"u8))
{
retB |= DrawDye(dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1);
}
}
DrawHeader(" Further Content"u8);
using (var columns = ImUtf8.Columns(2, "ColorTable"u8))
{
using var dis = ImRaii.Disabled(disabled);
using (ImUtf8.PushId("FurtherA"u8))
{
retA |= DrawFurther(table, dyeTable, dyePackA, _colorTableSelectedPair << 1);
}
columns.Next();
using (ImUtf8.PushId("FurtherB"u8))
{
retB |= DrawFurther(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1);
}
}
if (retA)
UpdateColorTableRowPreview(_colorTableSelectedPair << 1);
if (retB)
UpdateColorTableRowPreview((_colorTableSelectedPair << 1) | 1);
return retA | retB;
}
/// <remarks> Padding styles do not seem to apply to this component. It is recommended to prepend two spaces. </remarks>
private static void DrawHeader(ReadOnlySpan<byte> label)
{
var headerColor = ImGui.GetColorU32(ImGuiCol.Header);
using var _ = ImRaii.PushColor(ImGuiCol.HeaderHovered, headerColor).Push(ImGuiCol.HeaderActive, headerColor);
ImUtf8.CollapsingHeader(label, ImGuiTreeNodeFlags.Leaf);
}
private static bool DrawColors(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx)
{
var dyeOffset = ImGui.GetContentRegionAvail().X
+ ImGui.GetStyle().ItemSpacing.X
- ImGui.GetStyle().ItemInnerSpacing.X
- ImGui.GetFrameHeight() * 2.0f;
var ret = false;
ref var row = ref table[rowIdx];
var dye = dyeTable?[rowIdx] ?? default;
ret |= CtColorPicker("Diffuse Color"u8, default, row.DiffuseColor,
c => table[rowIdx].DiffuseColor = c);
if (dyeTable != null)
{
ImGui.SameLine(dyeOffset);
ret |= CtApplyStainCheckbox("##dyeDiffuseColor"u8, "Apply Diffuse Color on Dye"u8, dye.DiffuseColor,
b => dyeTable[rowIdx].DiffuseColor = b);
ImUtf8.SameLineInner();
CtColorPicker("##dyePreviewDiffuseColor"u8, "Dye Preview for Diffuse Color"u8, dyePack?.DiffuseColor);
}
ret |= CtColorPicker("Specular Color"u8, default, row.SpecularColor,
c => table[rowIdx].SpecularColor = c);
if (dyeTable != null)
{
ImGui.SameLine(dyeOffset);
ret |= CtApplyStainCheckbox("##dyeSpecularColor"u8, "Apply Specular Color on Dye"u8, dye.SpecularColor,
b => dyeTable[rowIdx].SpecularColor = b);
ImUtf8.SameLineInner();
CtColorPicker("##dyePreviewSpecularColor"u8, "Dye Preview for Specular Color"u8, dyePack?.SpecularColor);
}
ret |= CtColorPicker("Emissive Color"u8, default, row.EmissiveColor,
c => table[rowIdx].EmissiveColor = c);
if (dyeTable != null)
{
ImGui.SameLine(dyeOffset);
ret |= CtApplyStainCheckbox("##dyeEmissiveColor"u8, "Apply Emissive Color on Dye"u8, dye.EmissiveColor,
b => dyeTable[rowIdx].EmissiveColor = b);
ImUtf8.SameLineInner();
CtColorPicker("##dyePreviewEmissiveColor"u8, "Dye Preview for Emissive Color"u8, dyePack?.EmissiveColor);
}
return ret;
}
private static bool DrawBlending(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx)
{
var scalarSize = ColorTableScalarSize * UiHelpers.Scale;
var dyeOffset = ImGui.GetContentRegionAvail().X
+ ImGui.GetStyle().ItemSpacing.X
- ImGui.GetStyle().ItemInnerSpacing.X
- ImGui.GetFrameHeight()
- scalarSize;
var isRowB = (rowIdx & 1) != 0;
var ret = false;
ref var row = ref table[rowIdx];
var dye = dyeTable?[rowIdx] ?? default;
ImGui.SetNextItemWidth(scalarSize);
ret |= CtDragHalf(isRowB ? "Field #19"u8 : "Anisotropy Degree"u8, default, row.Anisotropy, "%.2f"u8, 0.0f, HalfMaxValue, 0.1f,
v => table[rowIdx].Anisotropy = v);
if (dyeTable != null)
{
ImGui.SameLine(dyeOffset);
ret |= CtApplyStainCheckbox("##dyeAnisotropy"u8, isRowB ? "Apply Field #19 on Dye"u8 : "Apply Anisotropy Degree on Dye"u8,
dye.Anisotropy,
b => dyeTable[rowIdx].Anisotropy = b);
ImUtf8.SameLineInner();
ImGui.SetNextItemWidth(scalarSize);
CtDragHalf("##dyePreviewAnisotropy"u8, isRowB ? "Dye Preview for Field #19"u8 : "Dye Preview for Anisotropy Degree"u8,
dyePack?.Anisotropy, "%.2f"u8);
}
return ret;
}
private bool DrawTemplate(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx)
{
var scalarSize = ColorTableScalarSize * UiHelpers.Scale;
var itemSpacing = ImGui.GetStyle().ItemSpacing.X;
var dyeOffset = ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() - scalarSize - 64.0f;
var subColWidth = CalculateSubColumnWidth(2);
var ret = false;
ref var row = ref table[rowIdx];
var dye = dyeTable?[rowIdx] ?? default;
ImGui.SetNextItemWidth(scalarSize);
ret |= CtDragScalar("Shader ID"u8, default, row.ShaderId, "%d"u8, (ushort)0, (ushort)255, 0.25f,
v => table[rowIdx].ShaderId = v);
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
ImGui.SetNextItemWidth(scalarSize + itemSpacing + 64.0f);
ret |= CtSphereMapIndexPicker("###SphereMapIndex"u8, default, row.SphereMapIndex, false,
v => table[rowIdx].SphereMapIndex = v);
ImUtf8.SameLineInner();
ImUtf8.Text("Sphere Map"u8);
if (dyeTable != null)
{
var textRectMin = ImGui.GetItemRectMin();
var textRectMax = ImGui.GetItemRectMax();
ImGui.SameLine(dyeOffset);
var cursor = ImGui.GetCursorScreenPos();
ImGui.SetCursorScreenPos(cursor with { Y = float.Lerp(textRectMin.Y, textRectMax.Y, 0.5f) - ImGui.GetFrameHeight() * 0.5f });
ret |= CtApplyStainCheckbox("##dyeSphereMapIndex"u8, "Apply Sphere Map on Dye"u8, dye.SphereMapIndex,
b => dyeTable[rowIdx].SphereMapIndex = b);
ImUtf8.SameLineInner();
ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() with { Y = cursor.Y });
ImGui.SetNextItemWidth(scalarSize + itemSpacing + 64.0f);
using var dis = ImRaii.Disabled();
CtSphereMapIndexPicker("###SphereMapIndexDye"u8, "Dye Preview for Sphere Map"u8, dyePack?.SphereMapIndex ?? ushort.MaxValue, false,
Nop);
}
ImGui.Dummy(new Vector2(64.0f, 0.0f));
ImGui.SameLine();
ImGui.SetNextItemWidth(scalarSize);
ret |= CtDragScalar("Sphere Map Intensity"u8, default, (float)row.SphereMapMask * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f,
HalfMaxValue * 100.0f, 1.0f,
v => table[rowIdx].SphereMapMask = (Half)(v * 0.01f));
if (dyeTable != null)
{
ImGui.SameLine(dyeOffset);
ret |= CtApplyStainCheckbox("##dyeSphereMapMask"u8, "Apply Sphere Map Intensity on Dye"u8, dye.SphereMapMask,
b => dyeTable[rowIdx].SphereMapMask = b);
ImUtf8.SameLineInner();
ImGui.SetNextItemWidth(scalarSize);
CtDragScalar("##dyeSphereMapMask"u8, "Dye Preview for Sphere Map Intensity"u8, (float?)dyePack?.SphereMapMask * 100.0f, "%.0f%%"u8);
}
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
var leftLineHeight = 64.0f + ImGui.GetStyle().FramePadding.Y * 2.0f;
var rightLineHeight = 3.0f * ImGui.GetFrameHeight() + 2.0f * ImGui.GetStyle().ItemSpacing.Y;
var lineHeight = Math.Max(leftLineHeight, rightLineHeight);
var cursorPos = ImGui.GetCursorScreenPos();
ImGui.SetCursorScreenPos(cursorPos + new Vector2(0.0f, (lineHeight - leftLineHeight) * 0.5f));
ImGui.SetNextItemWidth(scalarSize + (itemSpacing + 64.0f) * 2.0f);
ret |= CtTileIndexPicker("###TileIndex"u8, default, row.TileIndex, false,
v => table[rowIdx].TileIndex = v);
ImUtf8.SameLineInner();
ImUtf8.Text("Tile"u8);
ImGui.SameLine(subColWidth);
ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() with { Y = cursorPos.Y + (lineHeight - rightLineHeight) * 0.5f });
using (ImUtf8.Child("###TileProperties"u8,
new Vector2(ImGui.GetContentRegionAvail().X, float.Lerp(rightLineHeight, lineHeight, 0.5f))))
{
ImGui.Dummy(new Vector2(scalarSize, 0.0f));
ImUtf8.SameLineInner();
ImGui.SetNextItemWidth(scalarSize);
ret |= CtDragScalar("Tile Opacity"u8, default, (float)row.TileAlpha * 100.0f, "%.0f%%"u8, 0.0f, HalfMaxValue * 100.0f, 1.0f,
v => table[rowIdx].TileAlpha = (Half)(v * 0.01f));
ret |= CtTileTransformMatrix(row.TileTransform, scalarSize, true,
m => table[rowIdx].TileTransform = m);
ImUtf8.SameLineInner();
ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos()
- new Vector2(0.0f, (ImGui.GetFrameHeight() + ImGui.GetStyle().ItemSpacing.Y) * 0.5f));
ImUtf8.Text("Tile Transform"u8);
}
return ret;
}
private static bool DrawPbr(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx)
{
var scalarSize = ColorTableScalarSize * UiHelpers.Scale;
var subColWidth = CalculateSubColumnWidth(2) + ImGui.GetStyle().ItemSpacing.X;
var dyeOffset = subColWidth
- ImGui.GetStyle().ItemSpacing.X * 2.0f
- ImGui.GetStyle().ItemInnerSpacing.X
- ImGui.GetFrameHeight()
- scalarSize;
var ret = false;
ref var row = ref table[rowIdx];
var dye = dyeTable?[rowIdx] ?? default;
ImGui.SetNextItemWidth(scalarSize);
ret |= CtDragScalar("Roughness"u8, default, (float)row.Roughness * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f,
1.0f,
v => table[rowIdx].Roughness = (Half)(v * 0.01f));
if (dyeTable != null)
{
ImGui.SameLine(dyeOffset);
ret |= CtApplyStainCheckbox("##dyeRoughness"u8, "Apply Roughness on Dye"u8, dye.Roughness,
b => dyeTable[rowIdx].Roughness = b);
ImUtf8.SameLineInner();
ImGui.SetNextItemWidth(scalarSize);
CtDragScalar("##dyePreviewRoughness"u8, "Dye Preview for Roughness"u8, (float?)dyePack?.Roughness * 100.0f, "%.0f%%"u8);
}
ImGui.SameLine(subColWidth);
ImGui.SetNextItemWidth(scalarSize);
ret |= CtDragScalar("Metalness"u8, default, (float)row.Metalness * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f,
1.0f,
v => table[rowIdx].Metalness = (Half)(v * 0.01f));
if (dyeTable != null)
{
ImGui.SameLine(subColWidth + dyeOffset);
ret |= CtApplyStainCheckbox("##dyeMetalness"u8, "Apply Metalness on Dye"u8, dye.Metalness,
b => dyeTable[rowIdx].Metalness = b);
ImUtf8.SameLineInner();
ImGui.SetNextItemWidth(scalarSize);
CtDragScalar("##dyePreviewMetalness"u8, "Dye Preview for Metalness"u8, (float?)dyePack?.Metalness * 100.0f, "%.0f%%"u8);
}
return ret;
}
private static bool DrawSheen(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx)
{
var scalarSize = ColorTableScalarSize * UiHelpers.Scale;
var subColWidth = CalculateSubColumnWidth(2) + ImGui.GetStyle().ItemSpacing.X;
var dyeOffset = subColWidth
- ImGui.GetStyle().ItemSpacing.X * 2.0f
- ImGui.GetStyle().ItemInnerSpacing.X
- ImGui.GetFrameHeight()
- scalarSize;
var ret = false;
ref var row = ref table[rowIdx];
var dye = dyeTable?[rowIdx] ?? default;
ImGui.SetNextItemWidth(scalarSize);
ret |= CtDragScalar("Sheen"u8, default, (float)row.SheenRate * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f,
v => table[rowIdx].SheenRate = (Half)(v * 0.01f));
if (dyeTable != null)
{
ImGui.SameLine(dyeOffset);
ret |= CtApplyStainCheckbox("##dyeSheenRate"u8, "Apply Sheen on Dye"u8, dye.SheenRate,
b => dyeTable[rowIdx].SheenRate = b);
ImUtf8.SameLineInner();
ImGui.SetNextItemWidth(scalarSize);
CtDragScalar("##dyePreviewSheenRate"u8, "Dye Preview for Sheen"u8, (float?)dyePack?.SheenRate * 100.0f, "%.0f%%"u8);
}
ImGui.SameLine(subColWidth);
ImGui.SetNextItemWidth(scalarSize);
ret |= CtDragScalar("Sheen Tint"u8, default, (float)row.SheenTintRate * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f,
HalfMaxValue * 100.0f, 1.0f,
v => table[rowIdx].SheenTintRate = (Half)(v * 0.01f));
if (dyeTable != null)
{
ImGui.SameLine(subColWidth + dyeOffset);
ret |= CtApplyStainCheckbox("##dyeSheenTintRate"u8, "Apply Sheen Tint on Dye"u8, dye.SheenTintRate,
b => dyeTable[rowIdx].SheenTintRate = b);
ImUtf8.SameLineInner();
ImGui.SetNextItemWidth(scalarSize);
CtDragScalar("##dyePreviewSheenTintRate"u8, "Dye Preview for Sheen Tint"u8, (float?)dyePack?.SheenTintRate * 100.0f, "%.0f%%"u8);
}
ImGui.SetNextItemWidth(scalarSize);
ret |= CtDragScalar("Sheen Roughness"u8, default, 100.0f / (float)row.SheenAperture, "%.0f%%"u8, 100.0f / HalfMaxValue,
100.0f / HalfEpsilon, 1.0f,
v => table[rowIdx].SheenAperture = (Half)(100.0f / v));
if (dyeTable != null)
{
ImGui.SameLine(dyeOffset);
ret |= CtApplyStainCheckbox("##dyeSheenRoughness"u8, "Apply Sheen Roughness on Dye"u8, dye.SheenAperture,
b => dyeTable[rowIdx].SheenAperture = b);
ImUtf8.SameLineInner();
ImGui.SetNextItemWidth(scalarSize);
CtDragScalar("##dyePreviewSheenRoughness"u8, "Dye Preview for Sheen Roughness"u8, 100.0f / (float?)dyePack?.SheenAperture,
"%.0f%%"u8);
}
return ret;
}
private static bool DrawFurther(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx)
{
var scalarSize = ColorTableScalarSize * UiHelpers.Scale;
var subColWidth = CalculateSubColumnWidth(2) + ImGui.GetStyle().ItemSpacing.X;
var dyeOffset = subColWidth
- ImGui.GetStyle().ItemSpacing.X * 2.0f
- ImGui.GetStyle().ItemInnerSpacing.X
- ImGui.GetFrameHeight()
- scalarSize;
var ret = false;
ref var row = ref table[rowIdx];
var dye = dyeTable?[rowIdx] ?? default;
ImGui.SetNextItemWidth(scalarSize);
ret |= CtDragHalf("Field #11"u8, default, row.Scalar11, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f,
v => table[rowIdx].Scalar11 = v);
if (dyeTable != null)
{
ImGui.SameLine(dyeOffset);
ret |= CtApplyStainCheckbox("##dyeScalar11"u8, "Apply Field #11 on Dye"u8, dye.Scalar3,
b => dyeTable[rowIdx].Scalar3 = b);
ImUtf8.SameLineInner();
ImGui.SetNextItemWidth(scalarSize);
CtDragHalf("##dyePreviewScalar11"u8, "Dye Preview for Field #11"u8, dyePack?.Scalar3, "%.2f"u8);
}
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
ImGui.SetNextItemWidth(scalarSize);
ret |= CtDragHalf("Field #3"u8, default, row.Scalar3, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f,
v => table[rowIdx].Scalar3 = v);
ImGui.SameLine(subColWidth);
ImGui.SetNextItemWidth(scalarSize);
ret |= CtDragHalf("Field #7"u8, default, row.Scalar7, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f,
v => table[rowIdx].Scalar7 = v);
ImGui.SetNextItemWidth(scalarSize);
ret |= CtDragHalf("Field #15"u8, default, row.Scalar15, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f,
v => table[rowIdx].Scalar15 = v);
ImGui.SameLine(subColWidth);
ImGui.SetNextItemWidth(scalarSize);
ret |= CtDragHalf("Field #17"u8, default, row.Scalar17, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f,
v => table[rowIdx].Scalar17 = v);
ImGui.SetNextItemWidth(scalarSize);
ret |= CtDragHalf("Field #20"u8, default, row.Scalar20, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f,
v => table[rowIdx].Scalar20 = v);
ImGui.SameLine(subColWidth);
ImGui.SetNextItemWidth(scalarSize);
ret |= CtDragHalf("Field #22"u8, default, row.Scalar22, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f,
v => table[rowIdx].Scalar22 = v);
ImGui.SetNextItemWidth(scalarSize);
ret |= CtDragHalf("Field #23"u8, default, row.Scalar23, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f,
v => table[rowIdx].Scalar23 = v);
return ret;
}
private bool DrawDye(ColorDyeTable dyeTable, DyePack? dyePack, int rowIdx)
{
var scalarSize = ColorTableScalarSize * UiHelpers.Scale;
var applyButtonWidth = ImUtf8.CalcTextSize("Apply Preview Dye"u8).X + ImGui.GetStyle().FramePadding.X * 2.0f;
var subColWidth = CalculateSubColumnWidth(2, applyButtonWidth);
var ret = false;
ref var dye = ref dyeTable[rowIdx];
ImGui.SetNextItemWidth(scalarSize);
ret |= CtDragScalar("Dye Channel"u8, default, dye.Channel + 1, "%d"u8, 1, StainService.ChannelCount, 0.1f,
value => dyeTable[rowIdx].Channel = (byte)(Math.Clamp(value, 1, StainService.ChannelCount) - 1));
ImGui.SameLine(subColWidth);
ImGui.SetNextItemWidth(scalarSize);
if (_stainService.GudTemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty,
scalarSize + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton))
{
dye.Template = _stainService.LegacyTemplateCombo.CurrentSelection;
ret = true;
}
ImUtf8.SameLineInner();
ImUtf8.Text("Dye Template"u8);
ImGui.SameLine(ImGui.GetContentRegionAvail().X - applyButtonWidth + ImGui.GetStyle().ItemSpacing.X);
using var dis = ImRaii.Disabled(!dyePack.HasValue);
if (ImUtf8.Button("Apply Preview Dye"u8))
ret |= Mtrl.ApplyDyeToRow(_stainService.GudStmFile, [
_stainService.StainCombo1.CurrentSelection.Key,
_stainService.StainCombo2.CurrentSelection.Key,
], rowIdx);
return ret;
}
private static void CenteredTextInRest(string text)
=> AlignedTextInRest(text, 0.5f);
private static void AlignedTextInRest(string text, float alignment)
{
var width = ImGui.CalcTextSize(text).X;
ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() + new Vector2((ImGui.GetContentRegionAvail().X - width) * alignment, 0.0f));
ImGui.TextUnformatted(text);
}
private static float CalculateSubColumnWidth(int numSubColumns, float reservedSpace = 0.0f)
{
var itemSpacing = ImGui.GetStyle().ItemSpacing.X;
return (ImGui.GetContentRegionAvail().X - reservedSpace - itemSpacing * (numSubColumns - 1)) / numSubColumns + itemSpacing;
}
}

View file

@ -0,0 +1,540 @@
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 (!ImUtf8.CollapsingHeader("Color Table"u8, ImGuiTreeNodeFlags.DefaultOpen))
return false;
ColorTableCopyAllClipboardButton();
ImGui.SameLine();
var ret = ColorTablePasteAllClipboardButton(disabled);
if (!disabled)
{
ImGui.SameLine();
ImUtf8.IconDummy();
ImGui.SameLine();
ret |= ColorTableDyeableCheckbox();
}
if (Mtrl.DyeTable != null)
{
ImGui.SameLine();
ImUtf8.IconDummy();
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),
ColorTable table => DrawColorTable(table, Mtrl.DyeTable as ColorDyeTable, disabled),
_ => false,
};
return ret;
}
private void ColorTableCopyAllClipboardButton()
{
if (Mtrl.Table == null)
return;
if (!ImUtf8.Button("Export All Rows to Clipboard"u8, 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 = ImUtf8.Checkbox("Dyeable"u8, ref dyeable);
if (ret)
{
Mtrl.DyeTable = dyeable
? Mtrl.Table switch
{
ColorTable => new ColorDyeTable(),
LegacyColorTable => new LegacyColorDyeTable(),
_ => null,
}
: null;
UpdateColorTablePreview();
}
return ret;
}
private bool ColorTablePasteFromClipboardButton(int rowIdx, bool disabled)
{
if (Mtrl.Table == null)
return false;
if (!ImUtf8.IconButton(FontAwesomeIcon.Paste, "Import an exported row from your clipboard onto this row."u8,
ImGui.GetFrameHeight() * Vector2.One, disabled))
return false;
try
{
var text = ImGui.GetClipboardText();
var data = Convert.FromBase64String(text);
var row = Mtrl.Table.RowAsBytes(rowIdx);
var dyeRow = Mtrl.DyeTable != null ? Mtrl.DyeTable.RowAsBytes(rowIdx) : [];
if (data.Length != row.Length && data.Length != row.Length + dyeRow.Length)
return false;
data.AsSpan(0, row.Length).TryCopyTo(row);
data.AsSpan(row.Length).TryCopyTo(dyeRow);
UpdateColorTableRowPreview(rowIdx);
return true;
}
catch
{
return false;
}
}
private void ColorTableHighlightButton(int pairIdx, bool disabled)
{
ImUtf8.IconButton(FontAwesomeIcon.Crosshairs,
"Highlight this pair of rows on your character, if possible.\n\nHighlight colors can be configured in Penumbra's settings."u8,
ImGui.GetFrameHeight() * Vector2.One, disabled || _colorTablePreviewers.Count == 0);
if (ImGui.IsItemHovered())
HighlightColorTablePair(pairIdx);
else if (_highlightedColorTablePair == pairIdx)
CancelColorTableHighlight();
}
private static void CtBlendRect(Vector2 rcMin, Vector2 rcMax, uint topColor, uint bottomColor)
{
var style = ImGui.GetStyle();
var frameRounding = style.FrameRounding;
var frameThickness = style.FrameBorderSize;
var borderColor = ImGui.GetColorU32(ImGuiCol.Border);
var drawList = ImGui.GetWindowDrawList();
if (topColor == bottomColor)
{
drawList.AddRectFilled(rcMin, rcMax, topColor, frameRounding, ImDrawFlags.RoundCornersDefault);
}
else
{
drawList.AddRectFilled(
rcMin, rcMax with { Y = float.Lerp(rcMin.Y, rcMax.Y, 1.0f / 3) },
topColor, frameRounding, ImDrawFlags.RoundCornersTopLeft | ImDrawFlags.RoundCornersTopRight);
drawList.AddRectFilledMultiColor(
rcMin with { Y = float.Lerp(rcMin.Y, rcMax.Y, 1.0f / 3) },
rcMax with { Y = float.Lerp(rcMin.Y, rcMax.Y, 2.0f / 3) },
topColor, topColor, bottomColor, bottomColor);
drawList.AddRectFilled(
rcMin with { Y = float.Lerp(rcMin.Y, rcMax.Y, 2.0f / 3) }, rcMax,
bottomColor, frameRounding, ImDrawFlags.RoundCornersBottomLeft | ImDrawFlags.RoundCornersBottomRight);
}
drawList.AddRect(rcMin, rcMax, borderColor, frameRounding, ImDrawFlags.RoundCornersDefault, frameThickness);
}
private static bool CtColorPicker(ReadOnlySpan<byte> label, ReadOnlySpan<byte> description, HalfColor current, Action<HalfColor> setter,
ReadOnlySpan<byte> letter = default)
{
var ret = false;
var inputSqrt = PseudoSqrtRgb((Vector3)current);
var tmp = inputSqrt;
if (ImUtf8.ColorEdit(label, ref tmp,
ImGuiColorEditFlags.NoInputs
| ImGuiColorEditFlags.DisplayRGB
| ImGuiColorEditFlags.InputRGB
| ImGuiColorEditFlags.NoTooltip
| ImGuiColorEditFlags.HDR)
&& tmp != inputSqrt)
{
setter((HalfColor)PseudoSquareRgb(tmp));
ret = true;
}
if (letter.Length > 0 && ImGui.IsItemVisible())
{
var textSize = ImUtf8.CalcTextSize(letter);
var center = ImGui.GetItemRectMin() + (ImGui.GetItemRectSize() - textSize) / 2;
var textColor = inputSqrt.LengthSquared() < 0.25f ? 0x80FFFFFFu : 0x80000000u;
ImGui.GetWindowDrawList().AddText(letter, center, textColor);
}
ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description);
return ret;
}
private static void CtColorPicker(ReadOnlySpan<byte> label, ReadOnlySpan<byte> description, HalfColor? current,
ReadOnlySpan<byte> letter = default)
{
if (current.HasValue)
{
CtColorPicker(label, description, current.Value, Nop, letter);
}
else
{
var tmp = Vector4.Zero;
ImUtf8.ColorEdit(label, ref tmp,
ImGuiColorEditFlags.NoInputs
| ImGuiColorEditFlags.DisplayRGB
| ImGuiColorEditFlags.InputRGB
| ImGuiColorEditFlags.NoTooltip
| ImGuiColorEditFlags.HDR
| ImGuiColorEditFlags.AlphaPreview);
if (letter.Length > 0 && ImGui.IsItemVisible())
{
var textSize = ImUtf8.CalcTextSize(letter);
var center = ImGui.GetItemRectMin() + (ImGui.GetItemRectSize() - textSize) / 2;
ImGui.GetWindowDrawList().AddText(letter, center, 0x80000000u);
}
ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description);
}
}
private static bool CtApplyStainCheckbox(ReadOnlySpan<byte> label, ReadOnlySpan<byte> description, bool current, Action<bool> setter)
{
var tmp = current;
var result = ApplyStainCheckbox.Draw(label, ref tmp);
ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description);
if (!result || tmp == current)
return false;
setter(tmp);
return true;
}
private static bool CtDragHalf(ReadOnlySpan<byte> label, ReadOnlySpan<byte> description, Half value, ReadOnlySpan<byte> format, float min,
float max, float speed, Action<Half> setter)
{
var tmp = (float)value;
var result = ImUtf8.DragScalar(label, ref tmp, format, min, max, speed);
ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description);
if (!result)
return false;
var newValue = (Half)tmp;
if (newValue == value)
return false;
setter(newValue);
return true;
}
private static bool CtDragHalf(ReadOnlySpan<byte> label, ReadOnlySpan<byte> description, ref Half value, ReadOnlySpan<byte> format,
float min, float max, float speed)
{
var tmp = (float)value;
var result = ImUtf8.DragScalar(label, ref tmp, format, min, max, speed);
ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description);
if (!result)
return false;
var newValue = (Half)tmp;
if (newValue == value)
return false;
value = newValue;
return true;
}
private static void CtDragHalf(ReadOnlySpan<byte> label, ReadOnlySpan<byte> description, Half? value, ReadOnlySpan<byte> format)
{
using var _ = ImRaii.Disabled();
var valueOrDefault = value ?? Half.Zero;
var floatValue = (float)valueOrDefault;
CtDragHalf(label, description, valueOrDefault, value.HasValue ? format : "-"u8, floatValue, floatValue, 0.0f, Nop);
}
private static bool CtDragScalar<T>(ReadOnlySpan<byte> label, ReadOnlySpan<byte> description, T value, ReadOnlySpan<byte> format, T min,
T max, float speed, Action<T> setter) where T : unmanaged, INumber<T>
{
var tmp = value;
var result = ImUtf8.DragScalar(label, ref tmp, format, min, max, speed);
ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description);
if (!result || tmp == value)
return false;
setter(tmp);
return true;
}
private static bool CtDragScalar<T>(ReadOnlySpan<byte> label, ReadOnlySpan<byte> description, ref T value, ReadOnlySpan<byte> format, T min,
T max, float speed) where T : unmanaged, INumber<T>
{
var tmp = value;
var result = ImUtf8.DragScalar(label, ref tmp, format, min, max, speed);
ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description);
if (!result || tmp == value)
return false;
value = tmp;
return true;
}
private static void CtDragScalar<T>(ReadOnlySpan<byte> label, ReadOnlySpan<byte> description, T? value, ReadOnlySpan<byte> format)
where T : unmanaged, INumber<T>
{
using var _ = ImRaii.Disabled();
var valueOrDefault = value ?? T.Zero;
CtDragScalar(label, description, valueOrDefault, value.HasValue ? format : "-"u8, valueOrDefault, valueOrDefault, 0.0f, Nop);
}
private bool CtTileIndexPicker(ReadOnlySpan<byte> label, ReadOnlySpan<byte> description, ushort value, bool compact, Action<ushort> setter)
{
if (!_materialTemplatePickers.DrawTileIndexPicker(label, description, ref value, compact))
return false;
setter(value);
return true;
}
private bool CtSphereMapIndexPicker(ReadOnlySpan<byte> label, ReadOnlySpan<byte> description, ushort value, bool compact,
Action<ushort> setter)
{
if (!_materialTemplatePickers.DrawSphereMapIndexPicker(label, description, ref value, compact))
return false;
setter(value);
return true;
}
private bool CtTileTransformMatrix(HalfMatrix2x2 value, float floatSize, bool twoRowLayout, Action<HalfMatrix2x2> setter)
{
var ret = false;
if (_config.EditRawTileTransforms)
{
var tmp = value;
ImGui.SetNextItemWidth(floatSize);
ret |= CtDragHalf("##TileTransformUU"u8, "Tile Repeat U"u8, ref tmp.UU, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f);
ImUtf8.SameLineInner();
ImGui.SetNextItemWidth(floatSize);
ret |= CtDragHalf("##TileTransformVV"u8, "Tile Repeat V"u8, ref tmp.VV, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f);
if (!twoRowLayout)
ImUtf8.SameLineInner();
ImGui.SetNextItemWidth(floatSize);
ret |= CtDragHalf("##TileTransformUV"u8, "Tile Skew U"u8, ref tmp.UV, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f);
ImUtf8.SameLineInner();
ImGui.SetNextItemWidth(floatSize);
ret |= CtDragHalf("##TileTransformVU"u8, "Tile Skew V"u8, ref tmp.VU, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f);
if (!ret || tmp == value)
return false;
setter(tmp);
}
else
{
value.Decompose(out var scale, out var rotation, out var shear);
rotation *= 180.0f / MathF.PI;
shear *= 180.0f / MathF.PI;
ImGui.SetNextItemWidth(floatSize);
var scaleXChanged = CtDragScalar("##TileScaleU"u8, "Tile Scale U"u8, ref scale.X, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f);
var activated = ImGui.IsItemActivated();
var deactivated = ImGui.IsItemDeactivated();
ImUtf8.SameLineInner();
ImGui.SetNextItemWidth(floatSize);
var scaleYChanged = CtDragScalar("##TileScaleV"u8, "Tile Scale V"u8, ref scale.Y, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f);
activated |= ImGui.IsItemActivated();
deactivated |= ImGui.IsItemDeactivated();
if (!twoRowLayout)
ImUtf8.SameLineInner();
ImGui.SetNextItemWidth(floatSize);
var rotationChanged = CtDragScalar("##TileRotation"u8, "Tile Rotation"u8, ref rotation, "%.0f°"u8, -180.0f, 180.0f, 1.0f);
activated |= ImGui.IsItemActivated();
deactivated |= ImGui.IsItemDeactivated();
ImUtf8.SameLineInner();
ImGui.SetNextItemWidth(floatSize);
var shearChanged = CtDragScalar("##TileShear"u8, "Tile Shear"u8, ref shear, "%.0f°"u8, -90.0f, 90.0f, 1.0f);
activated |= ImGui.IsItemActivated();
deactivated |= ImGui.IsItemDeactivated();
if (deactivated)
_pinnedTileTransform = null;
else if (activated)
_pinnedTileTransform = (scale, rotation, shear);
ret = scaleXChanged | scaleYChanged | rotationChanged | shearChanged;
if (!ret)
return false;
if (_pinnedTileTransform.HasValue)
{
var (pinScale, pinRotation, pinShear) = _pinnedTileTransform.Value;
if (!scaleXChanged)
scale.X = pinScale.X;
if (!scaleYChanged)
scale.Y = pinScale.Y;
if (!rotationChanged)
rotation = pinRotation;
if (!shearChanged)
shear = pinShear;
}
var newValue = HalfMatrix2x2.Compose(scale, rotation * MathF.PI / 180.0f, shear * MathF.PI / 180.0f);
if (newValue == value)
return false;
setter(newValue);
}
return true;
}
/// <remarks> For use as setter of read-only fields. </remarks>
private static void Nop<T>(T _)
{ }
// Functions to deal with squared RGB values without making negatives useless.
internal static float PseudoSquareRgb(float x)
=> x < 0.0f ? -(x * x) : x * x;
internal static Vector3 PseudoSquareRgb(Vector3 vec)
=> new(PseudoSquareRgb(vec.X), PseudoSquareRgb(vec.Y), PseudoSquareRgb(vec.Z));
internal static Vector4 PseudoSquareRgb(Vector4 vec)
=> new(PseudoSquareRgb(vec.X), PseudoSquareRgb(vec.Y), PseudoSquareRgb(vec.Z), vec.W);
internal static float PseudoSqrtRgb(float x)
=> x < 0.0f ? -MathF.Sqrt(-x) : MathF.Sqrt(x);
internal static Vector3 PseudoSqrtRgb(Vector3 vec)
=> new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z));
internal static Vector4 PseudoSqrtRgb(Vector4 vec)
=> new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z), vec.W);
}

View file

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

View file

@ -0,0 +1,252 @@
using JetBrains.Annotations;
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;
}
}
[UsedImplicitly]
private sealed class DevkitShaderKeyValue
{
public string Label = string.Empty;
public string Description = string.Empty;
}
[UsedImplicitly]
private sealed class DevkitShaderKey
{
public string Label = string.Empty;
public string Description = string.Empty;
public Dictionary<uint, DevkitShaderKeyValue> Values = [];
}
[UsedImplicitly]
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,
}
[UsedImplicitly]
private sealed class DevkitConstantValue
{
public string Label = string.Empty;
public string Description = string.Empty;
public double Value = 0;
}
[UsedImplicitly]
private sealed class DevkitConstant
{
public uint Offset = 0;
public uint? Length = null;
public uint? ByteOffset = null;
public uint? ByteSize = null;
public string Group = string.Empty;
public string Label = string.Empty;
public string Description = string.Empty;
public DevkitConstantType Type = DevkitConstantType.Float;
public float? Minimum = null;
public float? Maximum = null;
public float Step = 0.0f;
public float StepFast = 0.0f;
public float? Speed = null;
public float RelativeSpeed = 0.0f;
public float Exponent = 1.0f;
public float Factor = 1.0f;
public float Bias = 0.0f;
public byte Precision = 3;
public bool Hex = false;
public bool Slider = true;
public bool Drag = true;
public string Unit = string.Empty;
public bool SquaredRgb = false;
public bool Clamped = false;
public DevkitConstantValue[] Values = [];
public uint EffectiveByteOffset
=> ByteOffset ?? Offset * ValueSize;
public uint? EffectiveByteSize
=> ByteSize ?? Length * ValueSize;
public unsafe uint ValueSize
=> Type switch
{
DevkitConstantType.Hidden => sizeof(byte),
DevkitConstantType.Float => sizeof(float),
DevkitConstantType.Integer => sizeof(float),
DevkitConstantType.Color => sizeof(float),
DevkitConstantType.Enum => sizeof(float),
DevkitConstantType.Int32 => sizeof(int),
DevkitConstantType.Int32Enum => sizeof(int),
DevkitConstantType.Int8 => sizeof(byte),
DevkitConstantType.Int8Enum => sizeof(byte),
DevkitConstantType.Int16 => sizeof(short),
DevkitConstantType.Int16Enum => sizeof(short),
DevkitConstantType.Int64 => sizeof(long),
DevkitConstantType.Int64Enum => sizeof(long),
DevkitConstantType.Half => (uint)sizeof(Half),
DevkitConstantType.Double => sizeof(double),
DevkitConstantType.TileIndex => sizeof(float),
DevkitConstantType.SphereMapIndex => sizeof(float),
_ => sizeof(float),
};
public IEditor<byte>? CreateEditor(MaterialTemplatePickers? materialTemplatePickers)
=> Type switch
{
DevkitConstantType.Hidden => null,
DevkitConstantType.Float => CreateFloatEditor<float>().AsByteEditor(),
DevkitConstantType.Integer => CreateIntegerEditor<int>().IntAsFloatEditor().AsByteEditor(),
DevkitConstantType.Color => ColorEditor.Get(!Clamped).WithExponent(SquaredRgb ? 2.0f : 1.0f).AsByteEditor(),
DevkitConstantType.Enum => CreateEnumEditor(float.CreateSaturating).AsByteEditor(),
DevkitConstantType.Int32 => CreateIntegerEditor<int>().AsByteEditor(),
DevkitConstantType.Int32Enum => CreateEnumEditor(ToInteger<int>).AsByteEditor(),
DevkitConstantType.Int8 => CreateIntegerEditor<byte>(),
DevkitConstantType.Int8Enum => CreateEnumEditor(ToInteger<byte>),
DevkitConstantType.Int16 => CreateIntegerEditor<short>().AsByteEditor(),
DevkitConstantType.Int16Enum => CreateEnumEditor(ToInteger<short>).AsByteEditor(),
DevkitConstantType.Int64 => CreateIntegerEditor<long>().AsByteEditor(),
DevkitConstantType.Int64Enum => CreateEnumEditor(ToInteger<long>).AsByteEditor(),
DevkitConstantType.Half => CreateFloatEditor<Half>().AsByteEditor(),
DevkitConstantType.Double => CreateFloatEditor<double>().AsByteEditor(),
DevkitConstantType.TileIndex => materialTemplatePickers?.TileIndexPicker ?? ConstantEditors.DefaultIntAsFloat,
DevkitConstantType.SphereMapIndex => materialTemplatePickers?.SphereMapIndexPicker ?? ConstantEditors.DefaultIntAsFloat,
_ => ConstantEditors.DefaultFloat,
};
private IEditor<T> CreateIntegerEditor<T>()
where T : unmanaged, INumber<T>
=> ((Drag || Slider) && !Hex
? Drag
? (IEditor<T>)DragEditor<T>.CreateInteger(ToInteger<T>(Minimum), ToInteger<T>(Maximum), Speed ?? 0.25f, RelativeSpeed,
Unit, 0)
: SliderEditor<T>.CreateInteger(ToInteger<T>(Minimum) ?? default, ToInteger<T>(Maximum) ?? default, Unit, 0)
: InputEditor<T>.CreateInteger(ToInteger<T>(Minimum), ToInteger<T>(Maximum), ToInteger<T>(Step), ToInteger<T>(StepFast),
Hex, Unit, 0))
.WithFactorAndBias(ToInteger<T>(Factor), ToInteger<T>(Bias));
private IEditor<T> CreateFloatEditor<T>()
where T : unmanaged, INumber<T>, IPowerFunctions<T>
=> (Drag || Slider
? Drag
? (IEditor<T>)DragEditor<T>.CreateFloat(ToFloat<T>(Minimum), ToFloat<T>(Maximum), Speed ?? 0.1f, RelativeSpeed,
Precision, Unit, 0)
: SliderEditor<T>.CreateFloat(ToFloat<T>(Minimum) ?? default, ToFloat<T>(Maximum) ?? default, Precision, Unit, 0)
: InputEditor<T>.CreateFloat(ToFloat<T>(Minimum), ToFloat<T>(Maximum), T.CreateSaturating(Step),
T.CreateSaturating(StepFast), Precision, Unit, 0))
.WithExponent(T.CreateSaturating(Exponent))
.WithFactorAndBias(T.CreateSaturating(Factor), T.CreateSaturating(Bias));
private EnumEditor<T> CreateEnumEditor<T>(Func<double, T> convertValue)
where T : unmanaged, IUtf8SpanFormattable, IEqualityOperators<T, T, bool>
=> new(Array.ConvertAll(Values, value => (ToUtf8(value.Label), convertValue(value.Value), ToUtf8(value.Description))));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static T ToInteger<T>(float value) where T : struct, INumberBase<T>
=> T.CreateSaturating(MathF.Round(value));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static T ToInteger<T>(double value) where T : struct, INumberBase<T>
=> T.CreateSaturating(Math.Round(value));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static T? ToInteger<T>(float? value) where T : struct, INumberBase<T>
=> value.HasValue ? ToInteger<T>(value.Value) : null;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static T? ToFloat<T>(float? value) where T : struct, INumberBase<T>
=> value.HasValue ? T.CreateSaturating(value.Value) : null;
private static ReadOnlyMemory<byte> ToUtf8(string value)
=> Encoding.UTF8.GetBytes(value);
}
}

View file

@ -0,0 +1,380 @@
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 (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?[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 (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, LegacyColorDyeTableRow 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, ColorDyeTableRow dye, float floatSize)
{
var stain = _stainService.GetStainCombo(dye.Channel).CurrentSelection.Key;
if (stain == 0 || !_stainService.LegacyStmFile.TryGetValue(dye.Template, stain, out var values))
return false;
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing / 2);
var ret = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2(ImGui.GetFrameHeight()),
"Apply the selected dye to this row.", disabled, true);
ret = ret
&& Mtrl.ApplyDyeToRow(_stainService.LegacyStmFile, [
_stainService.StainCombo1.CurrentSelection.Key,
_stainService.StainCombo2.CurrentSelection.Key,
], rowIdx);
ImGui.SameLine();
DrawLegacyDyePreview(values, floatSize);
return ret;
}
private static void DrawLegacyDyePreview(LegacyDyePack values, float floatSize)
{
CtColorPicker("##diffusePreview"u8, default, values.DiffuseColor, "D"u8);
ImUtf8.SameLineInner();
CtColorPicker("##specularPreview"u8, default, values.SpecularColor, "S"u8);
ImUtf8.SameLineInner();
CtColorPicker("##emissivePreview"u8, default, values.EmissiveColor, "E"u8);
ImUtf8.SameLineInner();
using var dis = ImRaii.Disabled();
ImGui.SetNextItemWidth(floatSize);
var shininess = (float)values.Shininess;
ImGui.DragFloat("##shininessPreview", ref shininess, 0, shininess, shininess, "%.1f G");
ImUtf8.SameLineInner();
ImGui.SetNextItemWidth(floatSize);
var specularMask = (float)values.SpecularMask * 100.0f;
ImGui.DragFloat("##specularMaskPreview", ref specularMask, 0, specularMask, specularMask, "%.0f%% S");
}
}

View file

@ -0,0 +1,275 @@
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
{
private readonly List<LiveMaterialPreviewer> _materialPreviewers = new(4);
private readonly List<LiveColorTablePreviewer> _colorTablePreviewers = new(4);
private int _highlightedColorTablePair = -1;
private readonly Stopwatch _highlightTime = new();
private void DrawMaterialLivePreviewRebind(bool disabled)
{
if (disabled)
return;
if (ImUtf8.Button("Reload live preview"u8))
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);
}
private 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);
}
}
private void SetShaderPackageFlags(uint shPkFlags)
{
foreach (var previewer in _materialPreviewers)
previewer.SetShaderPackageFlags(shPkFlags);
}
private void SetMaterialParameter(uint parameterCrc, Index offset, Span<byte> value)
{
foreach (var previewer in _materialPreviewers)
previewer.SetMaterialParameter(parameterCrc, offset, value);
}
private 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);
}
private 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);
}
}
private void CancelColorTableHighlight()
{
var pairIdx = _highlightedColorTablePair;
_highlightedColorTablePair = -1;
_highlightTime.Reset();
if (pairIdx >= 0)
{
UpdateColorTableRowPreview(pairIdx << 1);
UpdateColorTableRowPreview((pairIdx << 1) | 1);
}
}
private void UpdateColorTableRowPreview(int rowIdx)
{
if (_colorTablePreviewers.Count == 0)
return;
if (Mtrl.Table == null)
return;
var row = Mtrl.Table switch
{
LegacyColorTable legacyTable => new ColorTableRow(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 ColorDyeTableRow(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();
}
}
private 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 ColorTableRow row, ColorId colorId, float time)
{
var level = (MathF.Sin(time * 2.0f * MathF.PI) + 2.0f) / 3.0f / 255.0f;
var baseColor = colorId.Value();
var color = level * new Vector3(baseColor & 0xFF, (baseColor >> 8) & 0xFF, (baseColor >> 16) & 0xFF);
var halfColor = (HalfColor)(color * color);
row.DiffuseColor = halfColor;
row.SpecularColor = halfColor;
row.EmissiveColor = halfColor;
}
}

View file

@ -0,0 +1,507 @@
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
private static readonly IReadOnlyList<string> StandardShaderPackages =
[
"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 = "Vertex Shaders: ???\nPixel Shaders: ???"u8.ToArray();
private string[]? _shpkNames;
private string _shaderHeader = "Shader###Shader";
private FullPath _loadedShpkPath = FullPath.Empty;
private string _loadedShpkPathName = string.Empty;
private string _loadedShpkDevkitPathName = string.Empty;
private string _shaderComment = string.Empty;
private ShpkFile? _associatedShpk;
private bool _shpkLoading;
private JObject? _associatedShpkDevkit;
private readonly string _loadedBaseDevkitPathName;
private readonly JObject? _associatedBaseDevkit;
// Shader Key State
private readonly
List<(string Label, int Index, string Description, bool MonoFont, IReadOnlyList<(string Label, uint Value, string Description)>
Values)> _shaderKeys = new(16);
private readonly HashSet<int> _vertexShaders = new(16);
private readonly HashSet<int> _pixelShaders = new(16);
private bool _shadersKnown;
private ReadOnlyMemory<byte> _shadersString = UnknownShadersString;
private 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;
}
private 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);
}
private 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, passVertexShader) in vertexShadersByPass)
{
if (builder.Length > 0)
builder.Append("\n\n");
var passName = Names.KnownNames.TryResolve(passId);
var shaders = passVertexShader.OrderBy(i => i).Select(i => $"#{i}");
builder.Append($"Vertex Shaders ({passName}): {string.Join(", ", shaders)}");
if (pixelShadersByPass.TryGetValue(passId, out var passPixelShader))
{
shaders = passPixelShader.OrderBy(i => i).Select(i => $"#{i}");
builder.Append($"\nPixel Shaders ({passName}): {string.Join(", ", shaders)}");
}
}
foreach (var (passId, passPixelShader) in pixelShadersByPass)
{
if (vertexShadersByPass.ContainsKey(passId))
continue;
if (builder.Length > 0)
builder.Append("\n\n");
var passName = Names.KnownNames.TryResolve(passId);
var shaders = passPixelShader.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))
continue;
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));
ImUtf8.CopyOnClickSelectable(text, _loadedShpkPathName, tooltip);
ImUtf8.CopyOnClickSelectable(devkitText, _loadedShpkDevkitPathName, tooltip);
ImUtf8.CopyOnClickSelectable(baseDevkitText, _loadedBaseDevkitPathName, tooltip);
if (ImUtf8.Button("Associate Custom .shpk File"u8))
_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 (ImUtf8.ButtonEx("Associate Default .shpk File"u8, moddedPath.ToPath(), Vector2.Zero,
moddedPath.Equals(_loadedShpkPath)))
LoadShpk(moddedPath);
if (!gamePath.Path.Equals(moddedPath.InternalName))
{
ImGui.SameLine();
if (ImUtf8.ButtonEx("Associate Unmodded .shpk File", defaultPath, Vector2.Zero,
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];
using var id = ImUtf8.PushId((int)key.Category);
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 = ImUtf8.Combo(""u8, 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
ImUtf8.Text(label);
}
else if (description.Length > 0 || currentDescription.Length > 0)
{
ImUtf8.LabeledHelpMarker($"{label}: {currentLabel}",
description + (description.Length > 0 && currentDescription.Length > 0 ? "\n\n" : string.Empty) + currentDescription);
}
else
{
ImUtf8.Text($"{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));
ImUtf8.Text(_shaderComment);
}
}
}

View file

@ -0,0 +1,279 @@
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 Wrap(ref sampler.Flags);
ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f);
var addressMode = samplerFlags.UAddressMode;
if (ComboTextureAddressMode("##UAddressMode"u8, ref addressMode))
{
samplerFlags.UAddressMode = addressMode;
ret = true;
SetSamplerFlags(sampler.SamplerId, sampler.Flags);
}
ImGui.SameLine();
ImUtf8.LabeledHelpMarker("U Address Mode"u8, "Method to use for resolving a U texture coordinate that is outside the 0 to 1 range.");
ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f);
addressMode = samplerFlags.VAddressMode;
if (ComboTextureAddressMode("##VAddressMode"u8, ref addressMode))
{
samplerFlags.VAddressMode = addressMode;
ret = true;
SetSamplerFlags(sampler.SamplerId, sampler.Flags);
}
ImGui.SameLine();
ImUtf8.LabeledHelpMarker("V Address Mode"u8, "Method to use for resolving a V texture coordinate that is outside the 0 to 1 range.");
var lodBias = samplerFlags.LodBias;
ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f);
if (ImUtf8.DragScalar("##LoDBias"u8, ref lodBias, -8.0f, 7.984375f, 0.1f))
{
samplerFlags.LodBias = lodBias;
ret = true;
SetSamplerFlags(sampler.SamplerId, sampler.Flags);
}
ImGui.SameLine();
ImUtf8.LabeledHelpMarker("Level of Detail Bias"u8,
"Offset from the calculated mipmap level.\n\nHigher means that the texture will start to lose detail nearer.\nLower means that the texture will keep its detail until farther.");
var minLod = samplerFlags.MinLod;
ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f);
if (ImUtf8.DragScalar("##MinLoD"u8, ref minLod, 0, 15, 0.1f))
{
samplerFlags.MinLod = minLod;
ret = true;
SetSamplerFlags(sampler.SamplerId, sampler.Flags);
}
ImGui.SameLine();
ImUtf8.LabeledHelpMarker("Minimum Level of Detail"u8,
"Most detailed mipmap level to use.\n\n0 is the full-sized texture, 1 is the half-sized texture, 2 is the quarter-sized texture, and so on.\n15 will forcibly reduce the texture to its smallest mipmap.");
using var t = ImUtf8.TreeNode("Advanced Settings"u8);
if (!t)
return ret;
ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f);
if (ImUtf8.InputScalar("Texture Flags"u8, ref texture.Flags, "%04X"u8,
flags: disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))
ret = true;
ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f);
if (ImUtf8.InputScalar("Sampler Flags"u8, ref sampler.Flags, "%08X"u8,
flags: ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)))
{
ret = true;
SetSamplerFlags(sampler.SamplerId, sampler.Flags);
}
return ret;
}
}

View file

@ -0,0 +1,199 @@
using Dalamud.Plugin.Services;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Text;
using OtterGui.Widgets;
using Penumbra.GameData.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 (ImUtf8.Checkbox("Enable Transparency"u8, 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 (ImUtf8.Checkbox("Hide Backfaces"u8, 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 (!ImUtf8.CollapsingHeader("Further Content"u8))
return;
using (var sets = ImUtf8.TreeNode("UV Sets"u8, ImGuiTreeNodeFlags.DefaultOpen))
{
if (sets)
foreach (var set in Mtrl.UvSets)
ImUtf8.TreeNode($"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf).Dispose();
}
using (var sets = ImUtf8.TreeNode("Color Sets"u8, ImGuiTreeNodeFlags.DefaultOpen))
{
if (sets)
foreach (var set in Mtrl.ColorSets)
ImUtf8.TreeNode($"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf).Dispose();
}
if (Mtrl.AdditionalData.Length <= 0)
return;
using var t = ImUtf8.TreeNode($"Additional Data (Size: {Mtrl.AdditionalData.Length})###AdditionalData");
if (t)
Widget.DrawHexViewer(Mtrl.AdditionalData);
}
private void Update()
{
UpdateShaders();
UpdateTextures();
UpdateConstants();
}
public unsafe void Dispose()
{
UnbindFromMaterialInstances();
if (Writable)
_characterBaseDestructor.Unsubscribe(UnbindFromDrawObjectMaterialInstances);
}
public bool Valid
=> _shadersKnown && Mtrl.Valid;
public byte[] Write()
{
var output = Mtrl.Clone();
output.GarbageCollect(_associatedShpk, SamplerIds);
return output.Write();
}
}

View file

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

View file

@ -1,538 +0,0 @@
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Files;
using Penumbra.GameData.Files.MaterialStructs;
using Penumbra.String.Functions;
namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow
{
private static readonly float HalfMinValue = (float)Half.MinValue;
private static readonly float HalfMaxValue = (float)Half.MaxValue;
private static readonly float HalfEpsilon = (float)Half.Epsilon;
private bool DrawMaterialColorTableChange(MtrlTab tab, bool disabled)
{
if (!tab.SamplerIds.Contains(ShpkFile.TableSamplerId) || !tab.Mtrl.HasTable)
return false;
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
if (!ImGui.CollapsingHeader("Color Table", ImGuiTreeNodeFlags.DefaultOpen))
return false;
ColorTableCopyAllClipboardButton(tab.Mtrl);
ImGui.SameLine();
var ret = ColorTablePasteAllClipboardButton(tab, disabled);
if (!disabled)
{
ImGui.SameLine();
ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0));
ImGui.SameLine();
ret |= ColorTableDyeableCheckbox(tab);
}
var hasDyeTable = tab.Mtrl.HasDyeTable;
if (hasDyeTable)
{
ImGui.SameLine();
ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0));
ImGui.SameLine();
ret |= DrawPreviewDye(tab, disabled);
}
using var table = ImRaii.Table("##ColorTable", hasDyeTable ? 11 : 9,
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV);
if (!table)
return false;
ImGui.TableNextColumn();
ImGui.TableHeader(string.Empty);
ImGui.TableNextColumn();
ImGui.TableHeader("Row");
ImGui.TableNextColumn();
ImGui.TableHeader("Diffuse");
ImGui.TableNextColumn();
ImGui.TableHeader("Specular");
ImGui.TableNextColumn();
ImGui.TableHeader("Emissive");
ImGui.TableNextColumn();
ImGui.TableHeader("Gloss");
ImGui.TableNextColumn();
ImGui.TableHeader("Tile");
ImGui.TableNextColumn();
ImGui.TableHeader("Repeat");
ImGui.TableNextColumn();
ImGui.TableHeader("Skew");
if (hasDyeTable)
{
ImGui.TableNextColumn();
ImGui.TableHeader("Dye");
ImGui.TableNextColumn();
ImGui.TableHeader("Dye Preview");
}
for (var i = 0; i < ColorTable.NumRows; ++i)
{
ret |= DrawColorTableRow(tab, i, disabled);
ImGui.TableNextRow();
}
return ret;
}
private static void ColorTableCopyAllClipboardButton(MtrlFile file)
{
if (!ImGui.Button("Export All Rows to Clipboard", ImGuiHelpers.ScaledVector2(200, 0)))
return;
try
{
var data1 = file.Table.AsBytes();
var data2 = file.HasDyeTable ? file.DyeTable.AsBytes() : ReadOnlySpan<byte>.Empty;
var array = new byte[data1.Length + data2.Length];
data1.TryCopyTo(array);
data2.TryCopyTo(array.AsSpan(data1.Length));
var text = Convert.ToBase64String(array);
ImGui.SetClipboardText(text);
}
catch
{
// ignored
}
}
private bool DrawPreviewDye(MtrlTab tab, bool disabled)
{
var (dyeId, (name, dyeColor, gloss)) = _stainService.StainCombo.CurrentSelection;
var tt = dyeId == 0
? "Select a preview dye first."
: "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled.";
if (ImGuiUtil.DrawDisabledButton("Apply Preview Dye", Vector2.Zero, tt, disabled || dyeId == 0))
{
var ret = false;
if (tab.Mtrl.HasDyeTable)
for (var i = 0; i < LegacyColorTable.NumUsedRows; ++i)
ret |= tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, i, dyeId, 0);
tab.UpdateColorTablePreview();
return ret;
}
ImGui.SameLine();
var label = dyeId == 0 ? "Preview Dye###previewDye" : $"{name} (Preview)###previewDye";
if (_stainService.StainCombo.Draw(label, dyeColor, string.Empty, true, gloss))
tab.UpdateColorTablePreview();
return false;
}
private static unsafe bool ColorTablePasteAllClipboardButton(MtrlTab tab, bool disabled)
{
if (!ImGuiUtil.DrawDisabledButton("Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2(200, 0), string.Empty, disabled)
|| !tab.Mtrl.HasTable)
return false;
try
{
var text = ImGui.GetClipboardText();
var data = Convert.FromBase64String(text);
if (data.Length < Marshal.SizeOf<ColorTable>())
return false;
ref var rows = ref tab.Mtrl.Table;
fixed (void* ptr = data, output = &rows)
{
MemoryUtility.MemCpyUnchecked(output, ptr, Marshal.SizeOf<ColorTable>());
if (data.Length >= Marshal.SizeOf<ColorTable>() + Marshal.SizeOf<ColorDyeTable>()
&& tab.Mtrl.HasDyeTable)
{
ref var dyeRows = ref tab.Mtrl.DyeTable;
fixed (void* output2 = &dyeRows)
{
MemoryUtility.MemCpyUnchecked(output2, (byte*)ptr + Marshal.SizeOf<ColorTable>(),
Marshal.SizeOf<ColorDyeTable>());
}
}
}
tab.UpdateColorTablePreview();
return true;
}
catch
{
return false;
}
}
[SkipLocalsInit]
private static unsafe void ColorTableCopyClipboardButton(ColorTableRow row, ColorDyeTableRow 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[ColorTableRow.Size + ColorDyeTableRow.Size];
fixed (byte* ptr = data)
{
MemoryUtility.MemCpyUnchecked(ptr, &row, ColorTableRow.Size);
MemoryUtility.MemCpyUnchecked(ptr + ColorTableRow.Size, &dye, ColorDyeTableRow.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 != ColorTableRow.Size + ColorDyeTableRow.Size
|| !tab.Mtrl.HasTable)
return false;
fixed (byte* ptr = data)
{
tab.Mtrl.Table[rowIdx] = *(ColorTableRow*)ptr;
if (tab.Mtrl.HasDyeTable)
tab.Mtrl.DyeTable[rowIdx] = *(ColorDyeTableRow*)(ptr + ColorTableRow.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, ColorDyeTableRow dye, float floatSize)
{
var stain = _stainService.StainCombo.CurrentSelection.Key;
if (stain == 0 || !_stainService.StmFile.Entries.TryGetValue(dye.Template, out var entry))
return false;
var values = entry[(int)stain];
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing / 2);
var ret = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2(ImGui.GetFrameHeight()),
"Apply the selected dye to this row.", disabled, true);
ret = ret && tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, rowIdx, stain, 0);
if (ret)
tab.UpdateColorTableRowPreview(rowIdx);
ImGui.SameLine();
ColorPicker("##diffusePreview", string.Empty, values.Diffuse, _ => { }, "D");
ImGui.SameLine();
ColorPicker("##specularPreview", string.Empty, values.Specular, _ => { }, "S");
ImGui.SameLine();
ColorPicker("##emissivePreview", string.Empty, values.Emissive, _ => { }, "E");
ImGui.SameLine();
using var dis = ImRaii.Disabled();
ImGui.SetNextItemWidth(floatSize);
ImGui.DragFloat("##gloss", ref values.Gloss, 0, values.Gloss, values.Gloss, "%.1f G");
ImGui.SameLine();
ImGui.SetNextItemWidth(floatSize);
ImGui.DragFloat("##specularStrength", ref values.SpecularPower, 0, values.SpecularPower, values.SpecularPower, "%.2f S");
return ret;
}
private static bool ColorPicker(string label, string tooltip, Vector3 input, Action<Vector3> setter, string letter = "")
{
var ret = false;
var inputSqrt = PseudoSqrtRgb(input);
var tmp = inputSqrt;
if (ImGui.ColorEdit3(label, ref tmp,
ImGuiColorEditFlags.NoInputs
| ImGuiColorEditFlags.DisplayRGB
| ImGuiColorEditFlags.InputRGB
| ImGuiColorEditFlags.NoTooltip
| ImGuiColorEditFlags.HDR)
&& tmp != inputSqrt)
{
setter(PseudoSquareRgb(tmp));
ret = true;
}
if (letter.Length > 0 && ImGui.IsItemVisible())
{
var textSize = ImGui.CalcTextSize(letter);
var center = ImGui.GetItemRectMin() + (ImGui.GetItemRectSize() - textSize) / 2;
var textColor = input.LengthSquared() < 0.25f ? 0x80FFFFFFu : 0x80000000u;
ImGui.GetWindowDrawList().AddText(center, textColor, letter);
}
ImGuiUtil.HoverTooltip(tooltip, ImGuiHoveredFlags.AllowWhenDisabled);
return ret;
}
// Functions to deal with squared RGB values without making negatives useless.
private static float PseudoSquareRgb(float x)
=> x < 0.0f ? -(x * x) : x * x;
private static Vector3 PseudoSquareRgb(Vector3 vec)
=> new(PseudoSquareRgb(vec.X), PseudoSquareRgb(vec.Y), PseudoSquareRgb(vec.Z));
private static Vector4 PseudoSquareRgb(Vector4 vec)
=> new(PseudoSquareRgb(vec.X), PseudoSquareRgb(vec.Y), PseudoSquareRgb(vec.Z), vec.W);
private static float PseudoSqrtRgb(float x)
=> x < 0.0f ? -MathF.Sqrt(-x) : MathF.Sqrt(x);
internal static Vector3 PseudoSqrtRgb(Vector3 vec)
=> new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z));
private static Vector4 PseudoSqrtRgb(Vector4 vec)
=> new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z), vec.W);
}

View file

@ -1,247 +0,0 @@
using ImGuiNET;
using OtterGui.Raii;
using OtterGui;
using Penumbra.GameData;
namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow
{
private interface IConstantEditor
{
bool Draw(Span<float> values, bool disabled);
}
private sealed class FloatConstantEditor : IConstantEditor
{
public static readonly FloatConstantEditor Default = new(null, null, 0.1f, 0.0f, 1.0f, 0.0f, 3, string.Empty);
private readonly float? _minimum;
private readonly float? _maximum;
private readonly float _speed;
private readonly float _relativeSpeed;
private readonly float _factor;
private readonly float _bias;
private readonly string _format;
public FloatConstantEditor(float? minimum, float? maximum, float speed, float relativeSpeed, float factor, float bias, byte precision,
string unit)
{
_minimum = minimum;
_maximum = maximum;
_speed = speed;
_relativeSpeed = relativeSpeed;
_factor = factor;
_bias = bias;
_format = $"%.{Math.Min(precision, (byte)9)}f";
if (unit.Length > 0)
_format = $"{_format} {unit.Replace("%", "%%")}";
}
public bool Draw(Span<float> values, bool disabled)
{
var spacing = ImGui.GetStyle().ItemInnerSpacing.X;
var fieldWidth = (ImGui.CalcItemWidth() - (values.Length - 1) * spacing) / values.Length;
var ret = false;
// Not using DragScalarN because of _relativeSpeed and other points of lost flexibility.
for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx)
{
if (valueIdx > 0)
ImGui.SameLine(0.0f, spacing);
ImGui.SetNextItemWidth(MathF.Round(fieldWidth * (valueIdx + 1)) - MathF.Round(fieldWidth * valueIdx));
var value = (values[valueIdx] - _bias) / _factor;
if (disabled)
{
ImGui.DragFloat($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), value, value, _format);
}
else
{
if (ImGui.DragFloat($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), _minimum ?? 0.0f,
_maximum ?? 0.0f, _format))
{
values[valueIdx] = Clamp(value) * _factor + _bias;
ret = true;
}
}
}
return ret;
}
private float Clamp(float value)
=> Math.Clamp(value, _minimum ?? float.NegativeInfinity, _maximum ?? float.PositiveInfinity);
}
private sealed class IntConstantEditor : IConstantEditor
{
private readonly int? _minimum;
private readonly int? _maximum;
private readonly float _speed;
private readonly float _relativeSpeed;
private readonly float _factor;
private readonly float _bias;
private readonly string _format;
public IntConstantEditor(int? minimum, int? maximum, float speed, float relativeSpeed, float factor, float bias, string unit)
{
_minimum = minimum;
_maximum = maximum;
_speed = speed;
_relativeSpeed = relativeSpeed;
_factor = factor;
_bias = bias;
_format = "%d";
if (unit.Length > 0)
_format = $"{_format} {unit.Replace("%", "%%")}";
}
public bool Draw(Span<float> values, bool disabled)
{
var spacing = ImGui.GetStyle().ItemInnerSpacing.X;
var fieldWidth = (ImGui.CalcItemWidth() - (values.Length - 1) * spacing) / values.Length;
var ret = false;
// Not using DragScalarN because of _relativeSpeed and other points of lost flexibility.
for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx)
{
if (valueIdx > 0)
ImGui.SameLine(0.0f, spacing);
ImGui.SetNextItemWidth(MathF.Round(fieldWidth * (valueIdx + 1)) - MathF.Round(fieldWidth * valueIdx));
var value = (int)Math.Clamp(MathF.Round((values[valueIdx] - _bias) / _factor), int.MinValue, int.MaxValue);
if (disabled)
{
ImGui.DragInt($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), value, value, _format);
}
else
{
if (ImGui.DragInt($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), _minimum ?? 0, _maximum ?? 0,
_format))
{
values[valueIdx] = Clamp(value) * _factor + _bias;
ret = true;
}
}
}
return ret;
}
private int Clamp(int value)
=> Math.Clamp(value, _minimum ?? int.MinValue, _maximum ?? int.MaxValue);
}
private sealed class ColorConstantEditor : IConstantEditor
{
private readonly bool _squaredRgb;
private readonly bool _clamped;
public ColorConstantEditor(bool squaredRgb, bool clamped)
{
_squaredRgb = squaredRgb;
_clamped = clamped;
}
public bool Draw(Span<float> values, bool disabled)
{
switch (values.Length)
{
case 3:
{
var value = new Vector3(values);
if (_squaredRgb)
value = PseudoSqrtRgb(value);
if (!ImGui.ColorEdit3("##0", ref value, ImGuiColorEditFlags.Float | (_clamped ? 0 : ImGuiColorEditFlags.HDR)) || disabled)
return false;
if (_squaredRgb)
value = PseudoSquareRgb(value);
if (_clamped)
value = Vector3.Clamp(value, Vector3.Zero, Vector3.One);
value.CopyTo(values);
return true;
}
case 4:
{
var value = new Vector4(values);
if (_squaredRgb)
value = PseudoSqrtRgb(value);
if (!ImGui.ColorEdit4("##0", ref value,
ImGuiColorEditFlags.Float | ImGuiColorEditFlags.AlphaPreviewHalf | (_clamped ? 0 : ImGuiColorEditFlags.HDR))
|| disabled)
return false;
if (_squaredRgb)
value = PseudoSquareRgb(value);
if (_clamped)
value = Vector4.Clamp(value, Vector4.Zero, Vector4.One);
value.CopyTo(values);
return true;
}
default: return FloatConstantEditor.Default.Draw(values, disabled);
}
}
}
private sealed class EnumConstantEditor : IConstantEditor
{
private readonly IReadOnlyList<(string Label, float Value, string Description)> _values;
public EnumConstantEditor(IReadOnlyList<(string Label, float Value, string Description)> values)
=> _values = values;
public bool Draw(Span<float> values, bool disabled)
{
var spacing = ImGui.GetStyle().ItemInnerSpacing.X;
var fieldWidth = (ImGui.CalcItemWidth() - (values.Length - 1) * spacing) / values.Length;
var ret = false;
for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx)
{
using var id = ImRaii.PushId(valueIdx);
if (valueIdx > 0)
ImGui.SameLine(0.0f, spacing);
ImGui.SetNextItemWidth(MathF.Round(fieldWidth * (valueIdx + 1)) - MathF.Round(fieldWidth * valueIdx));
var currentValue = values[valueIdx];
var currentLabel = _values.FirstOrNull(v => v.Value == currentValue)?.Label
?? currentValue.ToString(CultureInfo.CurrentCulture);
ret = disabled
? ImGui.InputText(string.Empty, ref currentLabel, (uint)currentLabel.Length, ImGuiInputTextFlags.ReadOnly)
: DrawCombo(currentLabel, ref values[valueIdx]);
}
return ret;
}
private bool DrawCombo(string label, ref float currentValue)
{
using var c = ImRaii.Combo(string.Empty, label);
if (!c)
return false;
var ret = false;
foreach (var (valueLabel, value, valueDescription) in _values)
{
if (ImGui.Selectable(valueLabel, value == currentValue))
{
currentValue = value;
ret = true;
}
if (valueDescription.Length > 0)
ImGuiUtil.SelectableHelpMarker(valueDescription);
}
return ret;
}
}
}

View file

@ -1,783 +0,0 @@
using Dalamud.Interface;
using Dalamud.Interface.ImGuiNotification;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using ImGuiNET;
using Newtonsoft.Json.Linq;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii;
using Penumbra.GameData.Data;
using Penumbra.GameData.Files;
using Penumbra.GameData.Files.MaterialStructs;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Hooks.Objects;
using Penumbra.Interop.MaterialPreview;
using Penumbra.String;
using Penumbra.String.Classes;
using Penumbra.UI.Classes;
using static Penumbra.GameData.Files.ShpkFile;
namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow
{
private sealed class MtrlTab : IWritable, IDisposable
{
private const int ShpkPrefixLength = 16;
private static readonly CiByteString ShpkPrefix = CiByteString.FromSpanUnsafe("shader/sm5/shpk/"u8, true, true, true);
private readonly ModEditWindow _edit;
public readonly MtrlFile Mtrl;
public readonly string FilePath;
public readonly bool Writable;
private string[]? _shpkNames;
public string ShaderHeader = "Shader###Shader";
public FullPath LoadedShpkPath = FullPath.Empty;
public string LoadedShpkPathName = string.Empty;
public string LoadedShpkDevkitPathName = string.Empty;
public string ShaderComment = string.Empty;
public ShpkFile? AssociatedShpk;
public JObject? AssociatedShpkDevkit;
public readonly string LoadedBaseDevkitPathName;
public readonly JObject? AssociatedBaseDevkit;
// Shader Key State
public readonly
List<(string Label, int Index, string Description, bool MonoFont, IReadOnlyList<(string Label, uint Value, string Description)>
Values)> ShaderKeys = new(16);
public readonly HashSet<int> VertexShaders = new(16);
public readonly HashSet<int> PixelShaders = new(16);
public bool ShadersKnown;
public string VertexShadersString = "Vertex Shaders: ???";
public string PixelShadersString = "Pixel Shaders: ???";
// Textures & Samplers
public readonly List<(string Label, int TextureIndex, int SamplerIndex, string Description, bool MonoFont)> Textures = new(4);
public readonly HashSet<int> UnfoldedTextures = new(4);
public readonly HashSet<uint> SamplerIds = new(16);
public float TextureLabelWidth;
// Material Constants
public readonly
List<(string Header, List<(string Label, int ConstantIndex, Range Slice, string Description, bool MonoFont, IConstantEditor Editor)>
Constants)> Constants = new(16);
// Live-Previewers
public readonly List<LiveMaterialPreviewer> MaterialPreviewers = new(4);
public readonly List<LiveColorTablePreviewer> ColorTablePreviewers = new(4);
public int HighlightedColorTableRow = -1;
public readonly Stopwatch HighlightTime = new();
public FullPath FindAssociatedShpk(out string defaultPath, out Utf8GamePath defaultGamePath)
{
defaultPath = GamePaths.Shader.ShpkPath(Mtrl.ShaderPackage.Name);
if (!Utf8GamePath.FromString(defaultPath, out defaultGamePath))
return FullPath.Empty;
return _edit.FindBestMatch(defaultGamePath);
}
public string[] GetShpkNames()
{
if (null != _shpkNames)
return _shpkNames;
var names = new HashSet<string>(StandardShaderPackages);
names.UnionWith(_edit.FindPathsStartingWith(ShpkPrefix).Select(path => path.ToString()[ShpkPrefixLength..]));
_shpkNames = names.ToArray();
Array.Sort(_shpkNames);
return _shpkNames;
}
public void LoadShpk(FullPath path)
{
ShaderHeader = $"Shader ({Mtrl.ShaderPackage.Name})###Shader";
try
{
LoadedShpkPath = path;
var data = LoadedShpkPath.IsRooted
? File.ReadAllBytes(LoadedShpkPath.FullName)
: _edit._gameData.GetFile(LoadedShpkPath.InternalName.ToString())?.Data;
AssociatedShpk = data?.Length > 0 ? new ShpkFile(data) : throw new Exception("Failure to load file data.");
LoadedShpkPathName = path.ToPath();
}
catch (Exception e)
{
LoadedShpkPath = FullPath.Empty;
LoadedShpkPathName = string.Empty;
AssociatedShpk = null;
Penumbra.Messager.NotificationMessage(e, $"Could not load {LoadedShpkPath.ToPath()}.", NotificationType.Error, false);
}
if (LoadedShpkPath.InternalName.IsEmpty)
{
AssociatedShpkDevkit = null;
LoadedShpkDevkitPathName = string.Empty;
}
else
{
AssociatedShpkDevkit =
TryLoadShpkDevkit(Path.GetFileNameWithoutExtension(Mtrl.ShaderPackage.Name), out LoadedShpkDevkitPathName);
}
UpdateShaderKeys();
Update();
}
private JObject? TryLoadShpkDevkit(string shpkBaseName, out string devkitPathName)
{
try
{
if (!Utf8GamePath.FromString("penumbra/shpk_devkit/" + shpkBaseName + ".json", out var devkitPath))
throw new Exception("Could not assemble ShPk dev-kit path.");
var devkitFullPath = _edit.FindBestMatch(devkitPath);
if (!devkitFullPath.IsRooted)
throw new Exception("Could not resolve ShPk dev-kit path.");
devkitPathName = devkitFullPath.FullName;
return JObject.Parse(File.ReadAllText(devkitFullPath.FullName));
}
catch
{
devkitPathName = string.Empty;
return null;
}
}
private T? TryGetShpkDevkitData<T>(string category, uint? id, bool mayVary) where T : class
=> TryGetShpkDevkitData<T>(AssociatedShpkDevkit, LoadedShpkDevkitPathName, category, id, mayVary)
?? TryGetShpkDevkitData<T>(AssociatedBaseDevkit, LoadedBaseDevkitPathName, category, id, mayVary);
private T? TryGetShpkDevkitData<T>(JObject? devkit, string devkitPathName, string category, uint? id, bool mayVary) where T : class
{
if (devkit == null)
return null;
try
{
var data = devkit[category];
if (id.HasValue)
data = data?[id.Value.ToString()];
if (mayVary && (data as JObject)?["Vary"] != null)
{
var selector = BuildSelector(data!["Vary"]!
.Select(key => (uint)key)
.Select(key => Mtrl.GetShaderKey(key)?.Value ?? AssociatedShpk!.GetMaterialKeyById(key)!.Value.DefaultValue));
var index = (int)data["Selectors"]![selector.ToString()]!;
data = data["Items"]![index];
}
return data?.ToObject(typeof(T)) as T;
}
catch (Exception e)
{
// Some element in the JSON was undefined or invalid (wrong type, key that doesn't exist in the ShPk, index out of range, …)
Penumbra.Log.Error($"Error while traversing the ShPk dev-kit file at {devkitPathName}: {e}");
return null;
}
}
private void UpdateShaderKeys()
{
ShaderKeys.Clear();
if (AssociatedShpk != null)
foreach (var key in AssociatedShpk.MaterialKeys)
{
var dkData = TryGetShpkDevkitData<DevkitShaderKey>("ShaderKeys", key.Id, false);
var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label);
var valueSet = new HashSet<uint>(key.Values);
if (dkData != null)
valueSet.UnionWith(dkData.Values.Keys);
var mtrlKeyIndex = Mtrl.FindOrAddShaderKey(key.Id, key.DefaultValue);
var values = valueSet.Select<uint, (string Label, uint Value, string Description)>(value =>
{
if (dkData != null && dkData.Values.TryGetValue(value, out var dkValue))
return (dkValue.Label.Length > 0 ? dkValue.Label : $"0x{value:X8}", value, dkValue.Description);
return ($"0x{value:X8}", value, string.Empty);
}).ToArray();
Array.Sort(values, (x, y) =>
{
if (x.Value == key.DefaultValue)
return -1;
if (y.Value == key.DefaultValue)
return 1;
return string.Compare(x.Label, y.Label, StringComparison.Ordinal);
});
ShaderKeys.Add((hasDkLabel ? dkData!.Label : $"0x{key.Id:X8}", mtrlKeyIndex, dkData?.Description ?? string.Empty,
!hasDkLabel, values));
}
else
foreach (var (key, index) in Mtrl.ShaderPackage.ShaderKeys.WithIndex())
ShaderKeys.Add(($"0x{key.Category:X8}", index, string.Empty, true, Array.Empty<(string, uint, string)>()));
}
private void UpdateShaders()
{
VertexShaders.Clear();
PixelShaders.Clear();
if (AssociatedShpk == null)
{
ShadersKnown = false;
}
else
{
ShadersKnown = true;
var systemKeySelectors = AllSelectors(AssociatedShpk.SystemKeys).ToArray();
var sceneKeySelectors = AllSelectors(AssociatedShpk.SceneKeys).ToArray();
var subViewKeySelectors = AllSelectors(AssociatedShpk.SubViewKeys).ToArray();
var materialKeySelector =
BuildSelector(AssociatedShpk.MaterialKeys.Select(key => Mtrl.GetOrAddShaderKey(key.Id, key.DefaultValue).Value));
foreach (var systemKeySelector in systemKeySelectors)
{
foreach (var sceneKeySelector in sceneKeySelectors)
{
foreach (var subViewKeySelector in subViewKeySelectors)
{
var selector = BuildSelector(systemKeySelector, sceneKeySelector, materialKeySelector, subViewKeySelector);
var node = AssociatedShpk.GetNodeBySelector(selector);
if (node.HasValue)
foreach (var pass in node.Value.Passes)
{
VertexShaders.Add((int)pass.VertexShader);
PixelShaders.Add((int)pass.PixelShader);
}
else
ShadersKnown = false;
}
}
}
}
var vertexShaders = VertexShaders.OrderBy(i => i).Select(i => $"#{i}");
var pixelShaders = PixelShaders.OrderBy(i => i).Select(i => $"#{i}");
VertexShadersString = $"Vertex Shaders: {string.Join(", ", ShadersKnown ? vertexShaders : vertexShaders.Append("???"))}";
PixelShadersString = $"Pixel Shaders: {string.Join(", ", ShadersKnown ? pixelShaders : pixelShaders.Append("???"))}";
ShaderComment = TryGetShpkDevkitData<string>("Comment", null, true) ?? string.Empty;
}
private void UpdateTextures()
{
Textures.Clear();
SamplerIds.Clear();
if (AssociatedShpk == null)
{
SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId));
if (Mtrl.HasTable)
SamplerIds.Add(TableSamplerId);
foreach (var (sampler, index) in Mtrl.ShaderPackage.Samplers.WithIndex())
Textures.Add(($"0x{sampler.SamplerId:X8}", sampler.TextureIndex, index, string.Empty, true));
}
else
{
foreach (var index in VertexShaders)
SamplerIds.UnionWith(AssociatedShpk.VertexShaders[index].Samplers.Select(sampler => sampler.Id));
foreach (var index in PixelShaders)
SamplerIds.UnionWith(AssociatedShpk.PixelShaders[index].Samplers.Select(sampler => sampler.Id));
if (!ShadersKnown)
{
SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId));
if (Mtrl.HasTable)
SamplerIds.Add(TableSamplerId);
}
foreach (var samplerId in SamplerIds)
{
var shpkSampler = AssociatedShpk.GetSamplerById(samplerId);
if (shpkSampler is not { Slot: 2 })
continue;
var dkData = TryGetShpkDevkitData<DevkitSampler>("Samplers", samplerId, true);
var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label);
var sampler = Mtrl.GetOrAddSampler(samplerId, dkData?.DefaultTexture ?? string.Empty, out var samplerIndex);
Textures.Add((hasDkLabel ? dkData!.Label : shpkSampler.Value.Name, sampler.TextureIndex, samplerIndex,
dkData?.Description ?? string.Empty, !hasDkLabel));
}
if (SamplerIds.Contains(TableSamplerId))
Mtrl.HasTable = true;
}
Textures.Sort((x, y) => string.CompareOrdinal(x.Label, y.Label));
TextureLabelWidth = 50f * UiHelpers.Scale;
float helpWidth;
using (var _ = ImRaii.PushFont(UiBuilder.IconFont))
{
helpWidth = ImGui.GetStyle().ItemSpacing.X + ImGui.CalcTextSize(FontAwesomeIcon.InfoCircle.ToIconString()).X;
}
foreach (var (label, _, _, description, monoFont) in Textures)
{
if (!monoFont)
TextureLabelWidth = Math.Max(TextureLabelWidth, ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f));
}
using (var _ = ImRaii.PushFont(UiBuilder.MonoFont))
{
foreach (var (label, _, _, description, monoFont) in Textures)
{
if (monoFont)
TextureLabelWidth = Math.Max(TextureLabelWidth,
ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f));
}
}
TextureLabelWidth = TextureLabelWidth / UiHelpers.Scale + 4;
}
private void UpdateConstants()
{
static List<T> FindOrAddGroup<T>(List<(string, List<T>)> groups, string name)
{
foreach (var (groupName, group) in groups)
{
if (string.Equals(name, groupName, StringComparison.Ordinal))
return group;
}
var newGroup = new List<T>(16);
groups.Add((name, newGroup));
return newGroup;
}
Constants.Clear();
if (AssociatedShpk == null)
{
var fcGroup = FindOrAddGroup(Constants, "Further Constants");
foreach (var (constant, index) in Mtrl.ShaderPackage.Constants.WithIndex())
{
var values = Mtrl.GetConstantValues(constant);
for (var i = 0; i < values.Length; i += 4)
{
fcGroup.Add(($"0x{constant.Id:X8}", index, i..Math.Min(i + 4, values.Length), string.Empty, true,
FloatConstantEditor.Default));
}
}
}
else
{
var prefix = AssociatedShpk.GetConstantById(MaterialParamsConstantId)?.Name ?? string.Empty;
foreach (var shpkConstant in AssociatedShpk.MaterialParams)
{
if ((shpkConstant.ByteSize & 0x3) != 0)
continue;
var constant = Mtrl.GetOrAddConstant(shpkConstant.Id, shpkConstant.ByteSize >> 2, out var constantIndex);
var values = Mtrl.GetConstantValues(constant);
var handledElements = new IndexSet(values.Length, false);
var dkData = TryGetShpkDevkitData<DevkitConstant[]>("Constants", shpkConstant.Id, true);
if (dkData != null)
foreach (var dkConstant in dkData)
{
var offset = (int)dkConstant.Offset;
var length = values.Length - offset;
if (dkConstant.Length.HasValue)
length = Math.Min(length, (int)dkConstant.Length.Value);
if (length <= 0)
continue;
var editor = dkConstant.CreateEditor();
if (editor != null)
FindOrAddGroup(Constants, dkConstant.Group.Length > 0 ? dkConstant.Group : "Further Constants")
.Add((dkConstant.Label, constantIndex, offset..(offset + length), dkConstant.Description, false, editor));
handledElements.AddRange(offset, length);
}
var fcGroup = FindOrAddGroup(Constants, "Further Constants");
foreach (var (start, end) in handledElements.Ranges(complement:true))
{
if ((shpkConstant.ByteOffset & 0x3) == 0)
{
var offset = shpkConstant.ByteOffset >> 2;
for (int i = (start & ~0x3) - (offset & 0x3), j = offset >> 2; i < end; i += 4, ++j)
{
var rangeStart = Math.Max(i, start);
var rangeEnd = Math.Min(i + 4, end);
if (rangeEnd > rangeStart)
fcGroup.Add((
$"{prefix}[{j:D2}]{VectorSwizzle((offset + rangeStart) & 0x3, (offset + rangeEnd - 1) & 0x3)} (0x{shpkConstant.Id:X8})",
constantIndex, rangeStart..rangeEnd, string.Empty, true, FloatConstantEditor.Default));
}
}
else
{
for (var i = start; i < end; i += 4)
{
fcGroup.Add(($"0x{shpkConstant.Id:X8}", constantIndex, i..Math.Min(i + 4, end), string.Empty, true,
FloatConstantEditor.Default));
}
}
}
}
}
Constants.RemoveAll(group => group.Constants.Count == 0);
Constants.Sort((x, y) =>
{
if (string.Equals(x.Header, "Further Constants", StringComparison.Ordinal))
return 1;
if (string.Equals(y.Header, "Further Constants", StringComparison.Ordinal))
return -1;
return string.Compare(x.Header, y.Header, StringComparison.Ordinal);
});
// HACK the Replace makes w appear after xyz, for the cbuffer-location-based naming scheme
foreach (var (_, group) in Constants)
{
group.Sort((x, y) => string.CompareOrdinal(
x.MonoFont ? x.Label.Replace("].w", "].{") : x.Label,
y.MonoFont ? y.Label.Replace("].w", "].{") : y.Label));
}
}
public unsafe void BindToMaterialInstances()
{
UnbindFromMaterialInstances();
var instances = MaterialInfo.FindMaterials(_edit._resourceTreeFactory.GetLocalPlayerRelatedCharacters().Select(ch => ch.Address),
FilePath);
var foundMaterials = new HashSet<nint>();
foreach (var materialInfo in instances)
{
var material = materialInfo.GetDrawObjectMaterial(_edit._objects);
if (foundMaterials.Contains((nint)material))
continue;
try
{
MaterialPreviewers.Add(new LiveMaterialPreviewer(_edit._objects, materialInfo));
foundMaterials.Add((nint)material);
}
catch (InvalidOperationException)
{
// Carry on without that previewer.
}
}
UpdateMaterialPreview();
if (!Mtrl.HasTable)
return;
foreach (var materialInfo in instances)
{
try
{
ColorTablePreviewers.Add(new LiveColorTablePreviewer(_edit._objects, _edit._framework, materialInfo));
}
catch (InvalidOperationException)
{
// Carry on without that previewer.
}
}
UpdateColorTablePreview();
}
private void UnbindFromMaterialInstances()
{
foreach (var previewer in MaterialPreviewers)
previewer.Dispose();
MaterialPreviewers.Clear();
foreach (var previewer in ColorTablePreviewers)
previewer.Dispose();
ColorTablePreviewers.Clear();
}
private unsafe void UnbindFromDrawObjectMaterialInstances(CharacterBase* characterBase)
{
for (var i = MaterialPreviewers.Count; i-- > 0;)
{
var previewer = MaterialPreviewers[i];
if (previewer.DrawObject != characterBase)
continue;
previewer.Dispose();
MaterialPreviewers.RemoveAt(i);
}
for (var i = ColorTablePreviewers.Count; i-- > 0;)
{
var previewer = ColorTablePreviewers[i];
if (previewer.DrawObject != characterBase)
continue;
previewer.Dispose();
ColorTablePreviewers.RemoveAt(i);
}
}
public void SetShaderPackageFlags(uint shPkFlags)
{
foreach (var previewer in MaterialPreviewers)
previewer.SetShaderPackageFlags(shPkFlags);
}
public void SetMaterialParameter(uint parameterCrc, Index offset, Span<float> value)
{
foreach (var previewer in MaterialPreviewers)
previewer.SetMaterialParameter(parameterCrc, offset, value);
}
public void SetSamplerFlags(uint samplerCrc, uint samplerFlags)
{
foreach (var previewer in MaterialPreviewers)
previewer.SetSamplerFlags(samplerCrc, samplerFlags);
}
private void UpdateMaterialPreview()
{
SetShaderPackageFlags(Mtrl.ShaderPackage.Flags);
foreach (var constant in Mtrl.ShaderPackage.Constants)
{
var values = Mtrl.GetConstantValues(constant);
if (values != null)
SetMaterialParameter(constant.Id, 0, values);
}
foreach (var sampler in Mtrl.ShaderPackage.Samplers)
SetSamplerFlags(sampler.SamplerId, sampler.Flags);
}
public void HighlightColorTableRow(int rowIdx)
{
var oldRowIdx = HighlightedColorTableRow;
if (HighlightedColorTableRow != rowIdx)
{
HighlightedColorTableRow = rowIdx;
HighlightTime.Restart();
}
if (oldRowIdx >= 0)
UpdateColorTableRowPreview(oldRowIdx);
if (rowIdx >= 0)
UpdateColorTableRowPreview(rowIdx);
}
public void CancelColorTableHighlight()
{
var rowIdx = HighlightedColorTableRow;
HighlightedColorTableRow = -1;
HighlightTime.Reset();
if (rowIdx >= 0)
UpdateColorTableRowPreview(rowIdx);
}
public void UpdateColorTableRowPreview(int rowIdx)
{
if (ColorTablePreviewers.Count == 0)
return;
if (!Mtrl.HasTable)
return;
var row = new LegacyColorTableRow(Mtrl.Table[rowIdx]);
if (Mtrl.HasDyeTable)
{
var stm = _edit._stainService.StmFile;
var dye = new LegacyColorDyeTableRow(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 LegacyColorTableRow row, float time)
{
var level = (MathF.Sin(time * 2.0f * MathF.PI) + 2.0f) / 3.0f / 255.0f;
var baseColor = ColorId.InGameHighlight.Value();
var color = level * new Vector3(baseColor & 0xFF, (baseColor >> 8) & 0xFF, (baseColor >> 16) & 0xFF);
row.Diffuse = Vector3.Zero;
row.Specular = Vector3.Zero;
row.Emissive = color * color;
}
public void Update()
{
UpdateShaders();
UpdateTextures();
UpdateConstants();
}
public unsafe MtrlTab(ModEditWindow edit, MtrlFile file, string filePath, bool writable)
{
_edit = edit;
Mtrl = file;
FilePath = filePath;
Writable = writable;
AssociatedBaseDevkit = TryLoadShpkDevkit("_base", out LoadedBaseDevkitPathName);
LoadShpk(FindAssociatedShpk(out _, out _));
if (writable)
{
_edit._characterBaseDestructor.Subscribe(UnbindFromDrawObjectMaterialInstances, CharacterBaseDestructor.Priority.MtrlTab);
BindToMaterialInstances();
}
}
public unsafe void Dispose()
{
UnbindFromMaterialInstances();
if (Writable)
_edit._characterBaseDestructor.Unsubscribe(UnbindFromDrawObjectMaterialInstances);
}
// TODO Readd ShadersKnown
public bool Valid
=> (true || ShadersKnown) && Mtrl.Valid;
public byte[] Write()
{
var output = Mtrl.Clone();
output.GarbageCollect(AssociatedShpk, SamplerIds);
return output.Write();
}
private sealed class DevkitShaderKeyValue
{
public string Label = string.Empty;
public string Description = string.Empty;
}
private sealed class DevkitShaderKey
{
public string Label = string.Empty;
public string Description = string.Empty;
public Dictionary<uint, DevkitShaderKeyValue> Values = new();
}
private sealed class DevkitSampler
{
public string Label = string.Empty;
public string Description = string.Empty;
public string DefaultTexture = string.Empty;
}
private enum DevkitConstantType
{
Hidden = -1,
Float = 0,
Integer = 1,
Color = 2,
Enum = 3,
}
private sealed class DevkitConstantValue
{
public string Label = string.Empty;
public string Description = string.Empty;
public float Value = 0;
}
private sealed class DevkitConstant
{
public uint Offset = 0;
public uint? Length = null;
public string Group = string.Empty;
public string Label = string.Empty;
public string Description = string.Empty;
public DevkitConstantType Type = DevkitConstantType.Float;
public float? Minimum = null;
public float? Maximum = null;
public float? Speed = null;
public float RelativeSpeed = 0.0f;
public float Factor = 1.0f;
public float Bias = 0.0f;
public byte Precision = 3;
public string Unit = string.Empty;
public bool SquaredRgb = false;
public bool Clamped = false;
public DevkitConstantValue[] Values = Array.Empty<DevkitConstantValue>();
public IConstantEditor? CreateEditor()
=> Type switch
{
DevkitConstantType.Hidden => null,
DevkitConstantType.Float => new FloatConstantEditor(Minimum, Maximum, Speed ?? 0.1f, RelativeSpeed, Factor, Bias, Precision,
Unit),
DevkitConstantType.Integer => new IntConstantEditor(ToInteger(Minimum), ToInteger(Maximum), Speed ?? 0.25f, RelativeSpeed,
Factor, Bias, Unit),
DevkitConstantType.Color => new ColorConstantEditor(SquaredRgb, Clamped),
DevkitConstantType.Enum => new EnumConstantEditor(Array.ConvertAll(Values,
value => (value.Label, value.Value, value.Description))),
_ => FloatConstantEditor.Default,
};
private static int? ToInteger(float? value)
=> value.HasValue ? (int)Math.Clamp(MathF.Round(value.Value), int.MinValue, int.MaxValue) : null;
}
}
}

View file

@ -1,481 +0,0 @@
using Dalamud.Interface;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData;
using Penumbra.String.Classes;
namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow
{
private readonly FileDialogService _fileDialog;
// strings path/to/the.exe | grep --fixed-strings '.shpk' | sort -u | sed -e 's#^shader/sm5/shpk/##'
// Apricot shader packages are unlisted because
// 1. they cause performance/memory issues when calculating the effective shader set
// 2. they probably aren't intended for use with materials anyway
private static readonly IReadOnlyList<string> StandardShaderPackages = new[]
{
"3dui.shpk",
// "apricot_decal_dummy.shpk",
// "apricot_decal_ring.shpk",
// "apricot_decal.shpk",
// "apricot_lightmodel.shpk",
// "apricot_model_dummy.shpk",
// "apricot_model_morph.shpk",
// "apricot_model.shpk",
// "apricot_powder_dummy.shpk",
// "apricot_powder.shpk",
// "apricot_shape_dummy.shpk",
// "apricot_shape.shpk",
"bgcolorchange.shpk",
"bgcrestchange.shpk",
"bgdecal.shpk",
"bg.shpk",
"bguvscroll.shpk",
"channeling.shpk",
"characterglass.shpk",
"charactershadowoffset.shpk",
"character.shpk",
"cloud.shpk",
"createviewposition.shpk",
"crystal.shpk",
"directionallighting.shpk",
"directionalshadow.shpk",
"grass.shpk",
"hair.shpk",
"iris.shpk",
"lightshaft.shpk",
"linelighting.shpk",
"planelighting.shpk",
"pointlighting.shpk",
"river.shpk",
"shadowmask.shpk",
"skin.shpk",
"spotlighting.shpk",
"verticalfog.shpk",
"water.shpk",
"weather.shpk",
};
private enum TextureAddressMode : uint
{
Wrap = 0,
Mirror = 1,
Clamp = 2,
Border = 3,
}
private static readonly IReadOnlyList<string> TextureAddressModeTooltips = new[]
{
"Tile the texture at every UV integer junction.\n\nFor example, for U values between 0 and 3, the texture is repeated three times.",
"Flip the texture at every UV integer junction.\n\nFor U values between 0 and 1, for example, the texture is addressed normally; between 1 and 2, the texture is mirrored; between 2 and 3, the texture is normal again; and so on.",
"Texture coordinates outside the range [0.0, 1.0] are set to the texture color at 0.0 or 1.0, respectively.",
"Texture coordinates outside the range [0.0, 1.0] are set to the border color (generally black).",
};
private static bool DrawPackageNameInput(MtrlTab tab, bool disabled)
{
if (disabled)
{
ImGui.TextUnformatted("Shader Package: " + tab.Mtrl.ShaderPackage.Name);
return false;
}
var ret = false;
ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f);
using var c = ImRaii.Combo("Shader Package", tab.Mtrl.ShaderPackage.Name);
if (c)
foreach (var value in tab.GetShpkNames())
{
if (ImGui.Selectable(value, value == tab.Mtrl.ShaderPackage.Name))
{
tab.Mtrl.ShaderPackage.Name = value;
ret = true;
tab.AssociatedShpk = null;
tab.LoadedShpkPath = FullPath.Empty;
tab.LoadShpk(tab.FindAssociatedShpk(out _, out _));
}
}
return ret;
}
private static bool DrawShaderFlagsInput(MtrlTab tab, bool disabled)
{
var shpkFlags = (int)tab.Mtrl.ShaderPackage.Flags;
ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f);
if (!ImGui.InputInt("Shader Flags", ref shpkFlags, 0, 0,
ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)))
return false;
tab.Mtrl.ShaderPackage.Flags = (uint)shpkFlags;
tab.SetShaderPackageFlags((uint)shpkFlags);
return true;
}
/// <summary>
/// Show the currently associated shpk file, if any, and the buttons to associate
/// a specific shpk from your drive, the modded shpk by path or the default shpk.
/// </summary>
private void DrawCustomAssociations(MtrlTab tab)
{
const string tooltip = "Click to copy file path to clipboard.";
var text = tab.AssociatedShpk == null
? "Associated .shpk file: None"
: $"Associated .shpk file: {tab.LoadedShpkPathName}";
var devkitText = tab.AssociatedShpkDevkit == null
? "Associated dev-kit file: None"
: $"Associated dev-kit file: {tab.LoadedShpkDevkitPathName}";
var baseDevkitText = tab.AssociatedBaseDevkit == null
? "Base dev-kit file: None"
: $"Base dev-kit file: {tab.LoadedBaseDevkitPathName}";
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
ImGuiUtil.CopyOnClickSelectable(text, tab.LoadedShpkPathName, tooltip);
ImGuiUtil.CopyOnClickSelectable(devkitText, tab.LoadedShpkDevkitPathName, tooltip);
ImGuiUtil.CopyOnClickSelectable(baseDevkitText, tab.LoadedBaseDevkitPathName, tooltip);
if (ImGui.Button("Associate Custom .shpk File"))
_fileDialog.OpenFilePicker("Associate Custom .shpk File...", ".shpk", (success, name) =>
{
if (success)
tab.LoadShpk(new FullPath(name[0]));
}, 1, Mod!.ModPath.FullName, false);
var moddedPath = tab.FindAssociatedShpk(out var defaultPath, out var gamePath);
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton("Associate Default .shpk File", Vector2.Zero, moddedPath.ToPath(),
moddedPath.Equals(tab.LoadedShpkPath)))
tab.LoadShpk(moddedPath);
if (!gamePath.Path.Equals(moddedPath.InternalName))
{
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton("Associate Unmodded .shpk File", Vector2.Zero, defaultPath,
gamePath.Path.Equals(tab.LoadedShpkPath.InternalName)))
tab.LoadShpk(new FullPath(gamePath));
}
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
}
private static bool DrawMaterialShaderKeys(MtrlTab tab, bool disabled)
{
if (tab.ShaderKeys.Count == 0)
return false;
var ret = false;
foreach (var (label, index, description, monoFont, values) in tab.ShaderKeys)
{
using var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont);
ref var key = ref tab.Mtrl.ShaderPackage.ShaderKeys[index];
var shpkKey = tab.AssociatedShpk?.GetMaterialKeyById(key.Category);
var currentValue = key.Value;
var (currentLabel, _, currentDescription) =
values.FirstOrNull(v => v.Value == currentValue) ?? ($"0x{currentValue:X8}", currentValue, string.Empty);
if (!disabled && shpkKey.HasValue)
{
ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f);
using (var c = ImRaii.Combo($"##{key.Category:X8}", currentLabel))
{
if (c)
foreach (var (valueLabel, value, valueDescription) in values)
{
if (ImGui.Selectable(valueLabel, value == currentValue))
{
key.Value = value;
ret = true;
tab.Update();
}
if (valueDescription.Length > 0)
ImGuiUtil.SelectableHelpMarker(valueDescription);
}
}
ImGui.SameLine();
if (description.Length > 0)
ImGuiUtil.LabeledHelpMarker(label, description);
else
ImGui.TextUnformatted(label);
}
else if (description.Length > 0 || currentDescription.Length > 0)
{
ImGuiUtil.LabeledHelpMarker($"{label}: {currentLabel}",
description + (description.Length > 0 && currentDescription.Length > 0 ? "\n\n" : string.Empty) + currentDescription);
}
else
{
ImGui.TextUnformatted($"{label}: {currentLabel}");
}
}
return ret;
}
private static void DrawMaterialShaders(MtrlTab tab)
{
if (tab.AssociatedShpk == null)
return;
ImRaii.TreeNode(tab.VertexShadersString, ImGuiTreeNodeFlags.Leaf).Dispose();
ImRaii.TreeNode(tab.PixelShadersString, ImGuiTreeNodeFlags.Leaf).Dispose();
if (tab.ShaderComment.Length > 0)
{
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
ImGui.TextUnformatted(tab.ShaderComment);
}
}
private static bool DrawMaterialConstants(MtrlTab tab, bool disabled)
{
if (tab.Constants.Count == 0)
return false;
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
if (!ImGui.CollapsingHeader("Material Constants"))
return false;
using var _ = ImRaii.PushId("MaterialConstants");
var ret = false;
foreach (var (header, group) in tab.Constants)
{
using var t = ImRaii.TreeNode(header, ImGuiTreeNodeFlags.DefaultOpen);
if (!t)
continue;
foreach (var (label, constantIndex, slice, description, monoFont, editor) in group)
{
var constant = tab.Mtrl.ShaderPackage.Constants[constantIndex];
var buffer = tab.Mtrl.GetConstantValues(constant);
if (buffer.Length > 0)
{
using var id = ImRaii.PushId($"##{constant.Id:X8}:{slice.Start}");
ImGui.SetNextItemWidth(250.0f);
if (editor.Draw(buffer[slice], disabled))
{
ret = true;
tab.SetMaterialParameter(constant.Id, slice.Start, buffer[slice]);
}
ImGui.SameLine();
using var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont);
if (description.Length > 0)
ImGuiUtil.LabeledHelpMarker(label, description);
else
ImGui.TextUnformatted(label);
}
}
}
return ret;
}
private static bool DrawMaterialSampler(MtrlTab tab, bool disabled, int textureIdx, int samplerIdx)
{
var ret = false;
ref var texture = ref tab.Mtrl.Textures[textureIdx];
ref var sampler = ref tab.Mtrl.ShaderPackage.Samplers[samplerIdx];
// FIXME this probably doesn't belong here
static unsafe bool InputHexUInt16(string label, ref ushort v, ImGuiInputTextFlags flags)
{
fixed (ushort* v2 = &v)
{
return ImGui.InputScalar(label, ImGuiDataType.U16, (nint)v2, nint.Zero, nint.Zero, "%04X", flags);
}
}
static bool ComboTextureAddressMode(string label, ref uint samplerFlags, int bitOffset)
{
var current = (TextureAddressMode)((samplerFlags >> bitOffset) & 0x3u);
using var c = ImRaii.Combo(label, current.ToString());
if (!c)
return false;
var ret = false;
foreach (var value in Enum.GetValues<TextureAddressMode>())
{
if (ImGui.Selectable(value.ToString(), value == current))
{
samplerFlags = (samplerFlags & ~(0x3u << bitOffset)) | ((uint)value << bitOffset);
ret = true;
}
ImGuiUtil.SelectableHelpMarker(TextureAddressModeTooltips[(int)value]);
}
return ret;
}
var dx11 = texture.DX11;
if (ImGui.Checkbox("Prepend -- to the file name on DirectX 11", ref dx11))
{
texture.DX11 = dx11;
ret = true;
}
ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f);
if (ComboTextureAddressMode("##UAddressMode", ref sampler.Flags, 2))
{
ret = true;
tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags);
}
ImGui.SameLine();
ImGuiUtil.LabeledHelpMarker("U Address Mode", "Method to use for resolving a U texture coordinate that is outside the 0 to 1 range.");
ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f);
if (ComboTextureAddressMode("##VAddressMode", ref sampler.Flags, 0))
{
ret = true;
tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags);
}
ImGui.SameLine();
ImGuiUtil.LabeledHelpMarker("V Address Mode", "Method to use for resolving a V texture coordinate that is outside the 0 to 1 range.");
var lodBias = ((int)(sampler.Flags << 12) >> 22) / 64.0f;
ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f);
if (ImGui.DragFloat("##LoDBias", ref lodBias, 0.1f, -8.0f, 7.984375f))
{
sampler.Flags = (uint)((sampler.Flags & ~0x000FFC00)
| ((uint)((int)Math.Round(Math.Clamp(lodBias, -8.0f, 7.984375f) * 64.0f) & 0x3FF) << 10));
ret = true;
tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags);
}
ImGui.SameLine();
ImGuiUtil.LabeledHelpMarker("Level of Detail Bias",
"Offset from the calculated mipmap level.\n\nHigher means that the texture will start to lose detail nearer.\nLower means that the texture will keep its detail until farther.");
var minLod = (int)((sampler.Flags >> 20) & 0xF);
ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f);
if (ImGui.DragInt("##MinLoD", ref minLod, 0.1f, 0, 15))
{
sampler.Flags = (uint)((sampler.Flags & ~0x00F00000) | ((uint)Math.Clamp(minLod, 0, 15) << 20));
ret = true;
tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags);
}
ImGui.SameLine();
ImGuiUtil.LabeledHelpMarker("Minimum Level of Detail",
"Most detailed mipmap level to use.\n\n0 is the full-sized texture, 1 is the half-sized texture, 2 is the quarter-sized texture, and so on.\n15 will forcibly reduce the texture to its smallest mipmap.");
using var t = ImRaii.TreeNode("Advanced Settings");
if (!t)
return ret;
ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f);
if (InputHexUInt16("Texture Flags", ref texture.Flags,
disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))
ret = true;
var samplerFlags = (int)sampler.Flags;
ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f);
if (ImGui.InputInt("Sampler Flags", ref samplerFlags, 0, 0,
ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)))
{
sampler.Flags = (uint)samplerFlags;
ret = true;
tab.SetSamplerFlags(sampler.SamplerId, (uint)samplerFlags);
}
return ret;
}
private bool DrawMaterialShader(MtrlTab tab, bool disabled)
{
var ret = false;
if (ImGui.CollapsingHeader(tab.ShaderHeader))
{
ret |= DrawPackageNameInput(tab, disabled);
ret |= DrawShaderFlagsInput(tab, disabled);
DrawCustomAssociations(tab);
ret |= DrawMaterialShaderKeys(tab, disabled);
DrawMaterialShaders(tab);
}
if (tab.AssociatedShpkDevkit == null)
{
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
GC.KeepAlive(tab);
var textColor = ImGui.GetColorU32(ImGuiCol.Text);
var textColorWarning =
(textColor & 0xFF000000u)
| ((textColor & 0x00FEFEFE) >> 1)
| (tab.AssociatedShpk == null ? 0x80u : 0x8080u); // Half red or yellow
using var c = ImRaii.PushColor(ImGuiCol.Text, textColorWarning);
ImGui.TextUnformatted(tab.AssociatedShpk == null
? "Unable to find a suitable .shpk file for cross-references. Some functionality will be missing."
: "No dev-kit file found for this material's shaders. Please install one for optimal editing experience, such as actual constant names instead of hexadecimal identifiers.");
}
return ret;
}
private static string? MaterialParamName(bool componentOnly, int offset)
{
if (offset < 0)
return null;
return (componentOnly, offset & 0x3) switch
{
(true, 0) => "x",
(true, 1) => "y",
(true, 2) => "z",
(true, 3) => "w",
(false, 0) => $"[{offset >> 2:D2}].x",
(false, 1) => $"[{offset >> 2:D2}].y",
(false, 2) => $"[{offset >> 2:D2}].z",
(false, 3) => $"[{offset >> 2:D2}].w",
_ => null,
};
}
private static string VectorSwizzle(int firstComponent, int lastComponent)
=> (firstComponent, lastComponent) switch
{
(0, 4) => " ",
(0, 0) => ".x ",
(0, 1) => ".xy ",
(0, 2) => ".xyz ",
(0, 3) => " ",
(1, 1) => ".y ",
(1, 2) => ".yz ",
(1, 3) => ".yzw ",
(2, 2) => ".z ",
(2, 3) => ".zw ",
(3, 3) => ".w ",
_ => string.Empty,
};
private static (string? Name, bool ComponentOnly) MaterialParamRangeName(string prefix, int valueOffset, int valueLength)
{
if (valueLength == 0 || valueOffset < 0)
return (null, false);
var firstVector = valueOffset >> 2;
var lastVector = (valueOffset + valueLength - 1) >> 2;
var firstComponent = valueOffset & 0x3;
var lastComponent = (valueOffset + valueLength - 1) & 0x3;
if (firstVector == lastVector)
return ($"{prefix}[{firstVector}]{VectorSwizzle(firstComponent, lastComponent)}", true);
var sb = new StringBuilder(128);
sb.Append($"{prefix}[{firstVector}]{VectorSwizzle(firstComponent, 3).TrimEnd()}");
for (var i = firstVector + 1; i < lastVector; ++i)
sb.Append($", [{i}]");
sb.Append($", [{lastVector}]{VectorSwizzle(0, lastComponent)}");
return (sb.ToString(), false);
}
}

View file

@ -4,10 +4,7 @@ using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Text;
using OtterGui.Widgets;
using Penumbra.GameData.Files;
using Penumbra.String.Classes;
using Penumbra.UI.Classes;
using Penumbra.UI.AdvancedWindow.Materials;
namespace Penumbra.UI.AdvancedWindow;
@ -17,177 +14,10 @@ public partial class ModEditWindow
private bool DrawMaterialPanel(MtrlTab tab, bool disabled)
{
DrawVersionUpdate(tab, disabled);
DrawMaterialLivePreviewRebind(tab, disabled);
if (tab.DrawVersionUpdate(disabled))
_materialTab.SaveFile();
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
var ret = DrawBackFaceAndTransparency(tab, disabled);
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
ret |= DrawMaterialShader(tab, disabled);
ret |= DrawMaterialTextureChange(tab, disabled);
ret |= DrawMaterialColorTableChange(tab, disabled);
ret |= DrawMaterialConstants(tab, disabled);
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
DrawOtherMaterialDetails(tab.Mtrl, disabled);
return !disabled && ret;
}
private void DrawVersionUpdate(MtrlTab tab, bool disabled)
{
if (disabled || tab.Mtrl.IsDawnTrail)
return;
if (!ImUtf8.ButtonEx("Update MTRL Version to Dawntrail"u8,
"Try using this if the material can not be loaded or should use legacy shaders.\n\nThis is not revertible."u8,
new Vector2(-0.1f, 0), false, 0, Colors.PressEnterWarningBg))
return;
tab.Mtrl.MigrateToDawntrail();
_materialTab.SaveFile();
}
private static void DrawMaterialLivePreviewRebind(MtrlTab tab, bool disabled)
{
if (disabled)
return;
if (ImGui.Button("Reload live preview"))
tab.BindToMaterialInstances();
if (tab.MaterialPreviewers.Count != 0 || tab.ColorTablePreviewers.Count != 0)
return;
ImGui.SameLine();
using var c = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder);
ImGui.TextUnformatted(
"The current material has not been found on your character. Please check the Import from Screen tab for more information.");
}
private static bool DrawMaterialTextureChange(MtrlTab tab, bool disabled)
{
if (tab.Textures.Count == 0)
return false;
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
if (!ImGui.CollapsingHeader("Textures and Samplers", ImGuiTreeNodeFlags.DefaultOpen))
return false;
var frameHeight = ImGui.GetFrameHeight();
var ret = false;
using var table = ImRaii.Table("##Textures", 3);
ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, frameHeight);
ImGui.TableSetupColumn("Path", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthFixed, tab.TextureLabelWidth * UiHelpers.Scale);
foreach (var (label, textureI, samplerI, description, monoFont) in tab.Textures)
{
using var _ = ImRaii.PushId(samplerI);
var tmp = tab.Mtrl.Textures[textureI].Path;
var unfolded = tab.UnfoldedTextures.Contains(samplerI);
ImGui.TableNextColumn();
if (ImGuiUtil.DrawDisabledButton((unfolded ? FontAwesomeIcon.CaretDown : FontAwesomeIcon.CaretRight).ToIconString(),
new Vector2(frameHeight),
"Settings for this texture and the associated sampler", false, true))
{
unfolded = !unfolded;
if (unfolded)
tab.UnfoldedTextures.Add(samplerI);
else
tab.UnfoldedTextures.Remove(samplerI);
}
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
if (ImGui.InputText(string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength,
disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)
&& tmp.Length > 0
&& tmp != tab.Mtrl.Textures[textureI].Path)
{
ret = true;
tab.Mtrl.Textures[textureI].Path = tmp;
}
ImGui.TableNextColumn();
using (ImRaii.PushFont(UiBuilder.MonoFont, monoFont))
{
ImGui.AlignTextToFramePadding();
if (description.Length > 0)
ImGuiUtil.LabeledHelpMarker(label, description);
else
ImGui.TextUnformatted(label);
}
if (unfolded)
{
ImGui.TableNextColumn();
ImGui.TableNextColumn();
ret |= DrawMaterialSampler(tab, disabled, textureI, samplerI);
ImGui.TableNextColumn();
}
}
return ret;
}
private static bool DrawBackFaceAndTransparency(MtrlTab tab, bool disabled)
{
const uint transparencyBit = 0x10;
const uint backfaceBit = 0x01;
var ret = false;
using var dis = ImRaii.Disabled(disabled);
var tmp = (tab.Mtrl.ShaderPackage.Flags & transparencyBit) != 0;
if (ImGui.Checkbox("Enable Transparency", ref tmp))
{
tab.Mtrl.ShaderPackage.Flags =
tmp ? tab.Mtrl.ShaderPackage.Flags | transparencyBit : tab.Mtrl.ShaderPackage.Flags & ~transparencyBit;
ret = true;
tab.SetShaderPackageFlags(tab.Mtrl.ShaderPackage.Flags);
}
ImGui.SameLine(200 * UiHelpers.Scale + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X);
tmp = (tab.Mtrl.ShaderPackage.Flags & backfaceBit) != 0;
if (ImGui.Checkbox("Hide Backfaces", ref tmp))
{
tab.Mtrl.ShaderPackage.Flags = tmp ? tab.Mtrl.ShaderPackage.Flags | backfaceBit : tab.Mtrl.ShaderPackage.Flags & ~backfaceBit;
ret = true;
tab.SetShaderPackageFlags(tab.Mtrl.ShaderPackage.Flags);
}
return ret;
}
private static void DrawOtherMaterialDetails(MtrlFile file, bool _)
{
if (!ImGui.CollapsingHeader("Further Content"))
return;
using (var sets = ImRaii.TreeNode("UV Sets", ImGuiTreeNodeFlags.DefaultOpen))
{
if (sets)
foreach (var set in file.UvSets)
ImRaii.TreeNode($"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf).Dispose();
}
using (var sets = ImRaii.TreeNode("Color Sets", ImGuiTreeNodeFlags.DefaultOpen))
{
if (sets)
foreach (var set in file.ColorSets)
ImRaii.TreeNode($"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf).Dispose();
}
if (file.AdditionalData.Length <= 0)
return;
using var t = ImRaii.TreeNode($"Additional Data (Size: {file.AdditionalData.Length})###AdditionalData");
if (t)
Widget.DrawHexViewer(file.AdditionalData);
return tab.DrawPanel(disabled);
}
private void DrawMaterialReassignmentTab()
@ -195,7 +25,7 @@ public partial class ModEditWindow
if (_editor.Files.Mdl.Count == 0)
return;
using var tab = ImRaii.TabItem("Material Reassignment");
using var tab = ImUtf8.TabItem("Material Reassignment"u8);
if (!tab)
return;
@ -203,45 +33,43 @@ public partial class ModEditWindow
MaterialSuffix.Draw(_editor, ImGuiHelpers.ScaledVector2(175, 0));
ImGui.NewLine();
using var child = ImRaii.Child("##mdlFiles", -Vector2.One, true);
using var child = ImUtf8.Child("##mdlFiles"u8, -Vector2.One, true);
if (!child)
return;
using var table = ImRaii.Table("##files", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.One);
using var table = ImUtf8.Table("##files"u8, 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.One);
if (!table)
return;
var iconSize = ImGui.GetFrameHeight() * Vector2.One;
foreach (var (info, idx) in _editor.MdlMaterialEditor.ModelFiles.WithIndex())
{
using var id = ImRaii.PushId(idx);
ImGui.TableNextColumn();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Save.ToIconString(), iconSize,
"Save the changed mdl file.\nUse at own risk!", !info.Changed, true))
if (ImUtf8.IconButton(FontAwesomeIcon.Save, "Save the changed mdl file.\nUse at own risk!"u8, disabled: !info.Changed))
info.Save(_editor.Compactor);
ImGui.TableNextColumn();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Recycle.ToIconString(), iconSize,
"Restore current changes to default.", !info.Changed, true))
if (ImUtf8.IconButton(FontAwesomeIcon.Recycle, "Restore current changes to default."u8, disabled: !info.Changed))
info.Restore();
ImGui.TableNextColumn();
ImGui.TextUnformatted(info.Path.FullName[(Mod!.ModPath.FullName.Length + 1)..]);
ImUtf8.Text(info.Path.InternalName.Span[(Mod!.ModPath.FullName.Length + 1)..]);
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(400 * UiHelpers.Scale);
var tmp = info.CurrentMaterials[0];
if (ImGui.InputText("##0", ref tmp, 64))
if (ImUtf8.InputText("##0"u8, ref tmp))
info.SetMaterial(tmp, 0);
for (var i = 1; i < info.Count; ++i)
{
using var id2 = ImUtf8.PushId(i);
ImGui.TableNextColumn();
ImGui.TableNextColumn();
ImGui.TableNextColumn();
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(400 * UiHelpers.Scale);
tmp = info.CurrentMaterials[i];
if (ImGui.InputText($"##{i}", ref tmp, 64))
if (ImUtf8.InputText(""u8, ref tmp))
info.SetMaterial(tmp, i);
}
}

View file

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

View file

@ -1,7 +1,6 @@
using Dalamud.Interface;
using Dalamud.Interface.ImGuiNotification;
using ImGuiNET;
using Lumina.Misc;
using OtterGui.Raii;
using OtterGui;
using OtterGui.Classes;
@ -11,6 +10,8 @@ using Penumbra.GameData.Interop;
using Penumbra.String;
using static Penumbra.GameData.Files.ShpkFile;
using OtterGui.Widgets;
using OtterGui.Text;
using Penumbra.GameData.Structs;
namespace Penumbra.UI.AdvancedWindow;
@ -24,6 +25,9 @@ public partial class ModEditWindow
{
DrawShaderPackageSummary(file);
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
DrawShaderPackageFilterSection(file);
var ret = false;
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
ret |= DrawShaderPackageShaderArray(file, "Vertex Shader", file.Shpk.VertexShaders, disabled);
@ -50,21 +54,18 @@ public partial class ModEditWindow
private static void DrawShaderPackageSummary(ShpkTab tab)
{
ImGui.TextUnformatted(tab.Header);
if (tab.Shpk.IsLegacy)
ImUtf8.Text("This legacy shader package will not work in the current version of the game. Do not attempt to load it.",
ImGuiUtil.HalfBlendText(0x80u)); // Half red
ImUtf8.Text(tab.Header);
if (!tab.Shpk.Disassembled)
{
var textColor = ImGui.GetColorU32(ImGuiCol.Text);
var textColorWarning = (textColor & 0xFF000000u) | ((textColor & 0x00FEFEFE) >> 1) | 0x80u; // Half red
using var c = ImRaii.PushColor(ImGuiCol.Text, textColorWarning);
ImGui.TextUnformatted("Your system doesn't support disassembling shaders. Some functionality will be missing.");
}
ImUtf8.Text("Your system doesn't support disassembling shaders. Some functionality will be missing.",
ImGuiUtil.HalfBlendText(0x80u)); // Half red
}
private static void DrawShaderExportButton(ShpkTab tab, string objectName, Shader shader, int idx)
{
if (!ImGui.Button($"Export Shader Program Blob ({shader.Blob.Length} bytes)"))
if (!ImUtf8.Button($"Export Shader Program Blob ({shader.Blob.Length} bytes)"))
return;
var defaultName = objectName[0] switch
@ -100,7 +101,7 @@ public partial class ModEditWindow
private static void DrawShaderImportButton(ShpkTab tab, string objectName, Shader[] shaders, int idx)
{
if (!ImGui.Button("Replace Shader Program Blob"))
if (!ImUtf8.Button("Replace Shader Program Blob"u8))
return;
tab.FileDialog.OpenFilePicker($"Replace {objectName} #{idx} Program Blob...", "Shader Program Blobs{.o,.cso,.dxbc,.dxil}",
@ -123,6 +124,7 @@ public partial class ModEditWindow
{
shaders[idx].UpdateResources(tab.Shpk);
tab.Shpk.UpdateResources();
tab.UpdateFilteredUsed();
}
catch (Exception e)
{
@ -138,8 +140,8 @@ public partial class ModEditWindow
private static unsafe void DrawRawDisassembly(Shader shader)
{
using var t2 = ImRaii.TreeNode("Raw Program Disassembly");
if (!t2)
using var tree = ImUtf8.TreeNode("Raw Program Disassembly"u8);
if (!tree)
return;
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
@ -149,16 +151,114 @@ public partial class ModEditWindow
ImGuiInputTextFlags.ReadOnly, null, null);
}
private static void DrawShaderUsage(ShpkTab tab, Shader shader)
{
using (var node = ImUtf8.TreeNode("Used with Shader Keys"u8))
{
if (node)
{
foreach (var (key, keyIdx) in shader.SystemValues!.WithIndex())
{
ImUtf8.TreeNode(
$"Used with System Key {tab.TryResolveName(tab.Shpk.SystemKeys[keyIdx].Id)} \u2208 {{ {tab.NameSetToString(key)} }}",
ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose();
}
foreach (var (key, keyIdx) in shader.SceneValues!.WithIndex())
{
ImUtf8.TreeNode(
$"Used with Scene Key {tab.TryResolveName(tab.Shpk.SceneKeys[keyIdx].Id)} \u2208 {{ {tab.NameSetToString(key)} }}",
ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose();
}
foreach (var (key, keyIdx) in shader.MaterialValues!.WithIndex())
{
ImUtf8.TreeNode(
$"Used with Material Key {tab.TryResolveName(tab.Shpk.MaterialKeys[keyIdx].Id)} \u2208 {{ {tab.NameSetToString(key)} }}",
ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose();
}
foreach (var (key, keyIdx) in shader.SubViewValues!.WithIndex())
{
ImUtf8.TreeNode($"Used with Sub-View Key #{keyIdx} \u2208 {{ {tab.NameSetToString(key)} }}",
ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose();
}
}
}
ImUtf8.TreeNode($"Used in Passes: {tab.NameSetToString(shader.Passes)}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose();
}
private static void DrawShaderPackageFilterSection(ShpkTab tab)
{
if (!ImUtf8.CollapsingHeader(tab.FilterPopCount == tab.FilterMaximumPopCount ? "Filters###Filters"u8 : "Filters (ACTIVE)###Filters"u8))
return;
foreach (var (key, keyIdx) in tab.Shpk.SystemKeys.WithIndex())
DrawShaderPackageFilterSet(tab, $"System Key {tab.TryResolveName(key.Id)}", ref tab.FilterSystemValues[keyIdx]);
foreach (var (key, keyIdx) in tab.Shpk.SceneKeys.WithIndex())
DrawShaderPackageFilterSet(tab, $"Scene Key {tab.TryResolveName(key.Id)}", ref tab.FilterSceneValues[keyIdx]);
foreach (var (key, keyIdx) in tab.Shpk.MaterialKeys.WithIndex())
DrawShaderPackageFilterSet(tab, $"Material Key {tab.TryResolveName(key.Id)}", ref tab.FilterMaterialValues[keyIdx]);
foreach (var (_, keyIdx) in tab.Shpk.SubViewKeys.WithIndex())
DrawShaderPackageFilterSet(tab, $"Sub-View Key #{keyIdx}", ref tab.FilterSubViewValues[keyIdx]);
DrawShaderPackageFilterSet(tab, "Passes", ref tab.FilterPasses);
}
private static void DrawShaderPackageFilterSet(ShpkTab tab, string label, ref SharedSet<uint, uint> values)
{
if (values.PossibleValues == null)
{
ImUtf8.TreeNode(label, ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose();
return;
}
using var node = ImUtf8.TreeNode(label);
if (!node)
return;
foreach (var value in values.PossibleValues)
{
var contains = values.Contains(value);
if (!ImUtf8.Checkbox($"{tab.TryResolveName(value)}", ref contains))
continue;
if (contains)
{
if (values.AddExisting(value))
{
++tab.FilterPopCount;
tab.UpdateFilteredUsed();
}
}
else
{
if (values.Remove(value))
{
--tab.FilterPopCount;
tab.UpdateFilteredUsed();
}
}
}
}
private static bool DrawShaderPackageShaderArray(ShpkTab tab, string objectName, Shader[] shaders, bool disabled)
{
if (shaders.Length == 0 || !ImGui.CollapsingHeader($"{objectName}s"))
if (shaders.Length == 0 || !ImUtf8.CollapsingHeader($"{objectName}s"))
return false;
var ret = false;
for (var idx = 0; idx < shaders.Length; ++idx)
{
var shader = shaders[idx];
using var t = ImRaii.TreeNode($"{objectName} #{idx}");
var shader = shaders[idx];
if (!tab.IsFilterMatch(shader))
continue;
using var t = ImUtf8.TreeNode($"{objectName} #{idx}");
if (!t)
continue;
@ -169,30 +269,34 @@ public partial class ModEditWindow
DrawShaderImportButton(tab, objectName, shaders, idx);
}
ret |= DrawShaderPackageResourceArray("Constant Buffers", "slot", true, shader.Constants, true);
ret |= DrawShaderPackageResourceArray("Samplers", "slot", false, shader.Samplers, true);
ret |= DrawShaderPackageResourceArray("Unordered Access Views", "slot", true, shader.Uavs, true);
ret |= DrawShaderPackageResourceArray("Constant Buffers", "slot", true, shader.Constants, false, true);
ret |= DrawShaderPackageResourceArray("Samplers", "slot", false, shader.Samplers, false, true);
if (!tab.Shpk.IsLegacy)
ret |= DrawShaderPackageResourceArray("Textures", "slot", false, shader.Textures, false, true);
ret |= DrawShaderPackageResourceArray("Unordered Access Views", "slot", true, shader.Uavs, false, true);
if (shader.DeclaredInputs != 0)
ImRaii.TreeNode($"Declared Inputs: {shader.DeclaredInputs}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose();
ImUtf8.TreeNode($"Declared Inputs: {shader.DeclaredInputs}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose();
if (shader.UsedInputs != 0)
ImRaii.TreeNode($"Used Inputs: {shader.UsedInputs}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose();
ImUtf8.TreeNode($"Used Inputs: {shader.UsedInputs}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose();
if (shader.AdditionalHeader.Length > 8)
{
using var t2 = ImRaii.TreeNode($"Additional Header (Size: {shader.AdditionalHeader.Length})###AdditionalHeader");
using var t2 = ImUtf8.TreeNode($"Additional Header (Size: {shader.AdditionalHeader.Length})###AdditionalHeader");
if (t2)
Widget.DrawHexViewer(shader.AdditionalHeader);
}
if (tab.Shpk.Disassembled)
DrawRawDisassembly(shader);
DrawShaderUsage(tab, shader);
}
return ret;
}
private static bool DrawShaderPackageResource(string slotLabel, bool withSize, ref Resource resource, bool disabled)
private static bool DrawShaderPackageResource(string slotLabel, bool withSize, ref Resource resource, bool hasFilter, bool disabled)
{
var ret = false;
if (!disabled)
@ -205,16 +309,31 @@ public partial class ModEditWindow
if (resource.Used == null)
return ret;
var usedString = UsedComponentString(withSize, resource);
var usedString = UsedComponentString(withSize, false, resource);
if (usedString.Length > 0)
ImRaii.TreeNode($"Used: {usedString}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose();
{
ImUtf8.TreeNode(hasFilter ? $"Globally Used: {usedString}" : $"Used: {usedString}",
ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose();
if (hasFilter)
{
var filteredUsedString = UsedComponentString(withSize, true, resource);
if (filteredUsedString.Length > 0)
ImUtf8.TreeNode($"Used within Filters: {filteredUsedString}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet)
.Dispose();
else
ImUtf8.TreeNode("Unused within Filters"u8, ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose();
}
}
else
ImRaii.TreeNode("Unused", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose();
{
ImUtf8.TreeNode(hasFilter ? "Globally Unused"u8 : "Unused"u8, ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose();
}
return ret;
}
private static bool DrawShaderPackageResourceArray(string arrayName, string slotLabel, bool withSize, Resource[] resources, bool disabled)
private static bool DrawShaderPackageResourceArray(string arrayName, string slotLabel, bool withSize, Resource[] resources, bool hasFilter,
bool disabled)
{
if (resources.Length == 0)
return false;
@ -230,10 +349,10 @@ public partial class ModEditWindow
var name = $"#{idx}: {buf.Name} (ID: 0x{buf.Id:X8}), {slotLabel}: {buf.Slot}"
+ (withSize ? $", size: {buf.Size} registers###{idx}: {buf.Name} (ID: 0x{buf.Id:X8})" : string.Empty);
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
using var t2 = ImRaii.TreeNode(name, !disabled || buf.Used != null ? 0 : ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet);
font.Dispose();
using var t2 = ImUtf8.TreeNode(name, !disabled || buf.Used != null ? 0 : ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet);
font.Pop();
if (t2)
ret |= DrawShaderPackageResource(slotLabel, withSize, ref buf, disabled);
ret |= DrawShaderPackageResource(slotLabel, withSize, ref buf, hasFilter, disabled);
}
return ret;
@ -246,7 +365,7 @@ public partial class ModEditWindow
+ new Vector2(ImGui.CalcTextSize(label).X + 3 * ImGui.GetStyle().ItemInnerSpacing.X + ImGui.GetFrameHeight(),
ImGui.GetStyle().FramePadding.Y);
var ret = ImGui.CollapsingHeader(label);
var ret = ImUtf8.CollapsingHeader(label);
ImGui.GetWindowDrawList()
.AddText(UiBuilder.DefaultFont, UiBuilder.DefaultFont.FontSize, pos, ImGui.GetColorU32(ImGuiCol.Text), "Layout");
return ret;
@ -259,7 +378,7 @@ public partial class ModEditWindow
if (isSizeWellDefined)
return true;
ImGui.TextUnformatted(materialParams.HasValue
ImUtf8.Text(materialParams.HasValue
? $"Buffer size mismatch: {file.MaterialParamsSize} bytes ≠ {materialParams.Value.Size} registers ({materialParams.Value.Size << 4} bytes)"
: $"Buffer size mismatch: {file.MaterialParamsSize} bytes, not a multiple of 16");
return false;
@ -267,8 +386,8 @@ public partial class ModEditWindow
private static bool DrawShaderPackageMaterialMatrix(ShpkTab tab, bool disabled)
{
ImGui.TextUnformatted(tab.Shpk.Disassembled
? "Parameter positions (continuations are grayed out, unused values are red):"
ImUtf8.Text(tab.Shpk.Disassembled
? "Parameter positions (continuations are grayed out, globally unused values are red, unused values within filters are yellow):"
: "Parameter positions (continuations are grayed out):");
using var table = ImRaii.Table("##MaterialParamLayout", 5,
@ -276,17 +395,14 @@ public partial class ModEditWindow
if (!table)
return false;
ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, 25 * UiHelpers.Scale);
ImGui.TableSetupColumn("x", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale);
ImGui.TableSetupColumn("y", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale);
ImGui.TableSetupColumn("z", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale);
ImGui.TableSetupColumn("w", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale);
ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, 40 * UiHelpers.Scale);
ImGui.TableSetupColumn("x", ImGuiTableColumnFlags.WidthFixed, 250 * UiHelpers.Scale);
ImGui.TableSetupColumn("y", ImGuiTableColumnFlags.WidthFixed, 250 * UiHelpers.Scale);
ImGui.TableSetupColumn("z", ImGuiTableColumnFlags.WidthFixed, 250 * UiHelpers.Scale);
ImGui.TableSetupColumn("w", ImGuiTableColumnFlags.WidthFixed, 250 * UiHelpers.Scale);
ImGui.TableHeadersRow();
var textColorStart = ImGui.GetColorU32(ImGuiCol.Text);
var textColorCont = (textColorStart & 0x00FFFFFFu) | ((textColorStart & 0xFE000000u) >> 1); // Half opacity
var textColorUnusedStart = (textColorStart & 0xFF000000u) | ((textColorStart & 0x00FEFEFE) >> 1) | 0x80u; // Half red
var textColorUnusedCont = (textColorUnusedStart & 0x00FFFFFFu) | ((textColorUnusedStart & 0xFE000000u) >> 1);
var textColorStart = ImGui.GetColorU32(ImGuiCol.Text);
var ret = false;
for (var i = 0; i < tab.Matrix.GetLength(0); ++i)
@ -296,22 +412,21 @@ public partial class ModEditWindow
for (var j = 0; j < 4; ++j)
{
var (name, tooltip, idx, colorType) = tab.Matrix[i, j];
var color = colorType switch
{
ShpkTab.ColorType.Unused => textColorUnusedStart,
ShpkTab.ColorType.Used => textColorStart,
ShpkTab.ColorType.Continuation => textColorUnusedCont,
ShpkTab.ColorType.Continuation | ShpkTab.ColorType.Used => textColorCont,
_ => textColorStart,
};
var color = textColorStart;
if (!colorType.HasFlag(ShpkTab.ColorType.Used))
color = ImGuiUtil.HalfBlend(color, 0x80u); // Half red
else if (!colorType.HasFlag(ShpkTab.ColorType.FilteredUsed))
color = ImGuiUtil.HalfBlend(color, 0x8080u); // Half yellow
if (colorType.HasFlag(ShpkTab.ColorType.Continuation))
color = ImGuiUtil.HalfTransparent(color); // Half opacity
using var _ = ImRaii.PushId(i * 4 + j);
var deletable = !disabled && idx >= 0;
using (var font = ImRaii.PushFont(UiBuilder.MonoFont, tooltip.Length > 0))
using (ImRaii.PushFont(UiBuilder.MonoFont, tooltip.Length > 0))
{
using (var c = ImRaii.PushColor(ImGuiCol.Text, color))
using (ImRaii.PushColor(ImGuiCol.Text, color))
{
ImGui.TableNextColumn();
ImGui.Selectable(name);
ImUtf8.Selectable(name);
if (deletable && ImGui.IsItemClicked(ImGuiMouseButton.Right) && ImGui.GetIO().KeyCtrl)
{
tab.Shpk.MaterialParams = tab.Shpk.MaterialParams.RemoveItems(idx);
@ -320,35 +435,66 @@ public partial class ModEditWindow
}
}
ImGuiUtil.HoverTooltip(tooltip);
ImUtf8.HoverTooltip(tooltip);
}
if (deletable)
ImGuiUtil.HoverTooltip("\nControl + Right-Click to remove.");
ImUtf8.HoverTooltip("\nControl + Right-Click to remove."u8);
}
}
return ret;
}
private static void DrawShaderPackageMaterialDevkitExport(ShpkTab tab)
{
if (!ImUtf8.Button("Export globally unused parameters as material dev-kit file"u8))
return;
tab.FileDialog.OpenSavePicker("Export material dev-kit file", ".json", $"{Path.GetFileNameWithoutExtension(tab.FilePath)}.json",
".json", DoSave, null, false);
return;
void DoSave(bool success, string path)
{
if (!success)
return;
try
{
File.WriteAllText(path, tab.ExportDevkit().ToString());
}
catch (Exception e)
{
Penumbra.Messager.NotificationMessage(e, $"Could not export dev-kit for {Path.GetFileName(tab.FilePath)} to {path}.",
NotificationType.Error, false);
return;
}
Penumbra.Messager.NotificationMessage(
$"Material dev-kit file for {Path.GetFileName(tab.FilePath)} exported successfully to {Path.GetFileName(path)}.",
NotificationType.Success, false);
}
}
private static void DrawShaderPackageMisalignedParameters(ShpkTab tab)
{
using var t = ImRaii.TreeNode("Misaligned / Overflowing Parameters");
using var t = ImUtf8.TreeNode("Misaligned / Overflowing Parameters"u8);
if (!t)
return;
using var _ = ImRaii.PushFont(UiBuilder.MonoFont);
foreach (var name in tab.MalformedParameters)
ImRaii.TreeNode(name, ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose();
ImUtf8.TreeNode(name, ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose();
}
private static void DrawShaderPackageStartCombo(ShpkTab tab)
{
using var s = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing);
using (var _ = ImRaii.PushFont(UiBuilder.MonoFont))
using (ImRaii.PushFont(UiBuilder.MonoFont))
{
ImGui.SetNextItemWidth(UiHelpers.Scale * 400);
using var c = ImRaii.Combo("##Start", tab.Orphans[tab.NewMaterialParamStart].Name);
using var c = ImUtf8.Combo("##Start", tab.Orphans[tab.NewMaterialParamStart].Name);
if (c)
foreach (var (start, idx) in tab.Orphans.WithIndex())
{
@ -358,7 +504,7 @@ public partial class ModEditWindow
}
ImGui.SameLine();
ImGui.TextUnformatted("Start");
ImUtf8.Text("Start"u8);
}
private static void DrawShaderPackageEndCombo(ShpkTab tab)
@ -367,7 +513,7 @@ public partial class ModEditWindow
using (var _ = ImRaii.PushFont(UiBuilder.MonoFont))
{
ImGui.SetNextItemWidth(UiHelpers.Scale * 400);
using var c = ImRaii.Combo("##End", tab.Orphans[tab.NewMaterialParamEnd].Name);
using var c = ImUtf8.Combo("##End", tab.Orphans[tab.NewMaterialParamEnd].Name);
if (c)
{
var current = tab.Orphans[tab.NewMaterialParamStart].Index;
@ -384,7 +530,7 @@ public partial class ModEditWindow
}
ImGui.SameLine();
ImGui.TextUnformatted("End");
ImUtf8.Text("End"u8);
}
private static bool DrawShaderPackageNewParameter(ShpkTab tab)
@ -396,23 +542,24 @@ public partial class ModEditWindow
DrawShaderPackageEndCombo(tab);
ImGui.SetNextItemWidth(UiHelpers.Scale * 400);
if (ImGui.InputText("Name", ref tab.NewMaterialParamName, 63))
tab.NewMaterialParamId = Crc32.Get(tab.NewMaterialParamName, 0xFFFFFFFFu);
var newName = tab.NewMaterialParamName.Value!;
if (ImUtf8.InputText("Name", ref newName))
tab.NewMaterialParamName = newName;
var tooltip = tab.UsedIds.Contains(tab.NewMaterialParamId)
? "The ID is already in use. Please choose a different name."
: string.Empty;
if (!ImGuiUtil.DrawDisabledButton($"Add ID 0x{tab.NewMaterialParamId:X8}", new Vector2(400 * UiHelpers.Scale, ImGui.GetFrameHeight()),
tooltip,
tooltip.Length > 0))
var tooltip = tab.UsedIds.Contains(tab.NewMaterialParamName.Crc32)
? "The ID is already in use. Please choose a different name."u8
: ""u8;
if (!ImUtf8.ButtonEx($"Add {tab.NewMaterialParamName} (0x{tab.NewMaterialParamName.Crc32:X8})", tooltip,
new Vector2(400 * UiHelpers.Scale, ImGui.GetFrameHeight()), tooltip.Length > 0))
return false;
tab.Shpk.MaterialParams = tab.Shpk.MaterialParams.AddItem(new MaterialParam
{
Id = tab.NewMaterialParamId,
Id = tab.NewMaterialParamName.Crc32,
ByteOffset = (ushort)(tab.Orphans[tab.NewMaterialParamStart].Index << 2),
ByteSize = (ushort)((tab.NewMaterialParamEnd - tab.NewMaterialParamStart + 1) << 2),
});
tab.AddNameToCache(tab.NewMaterialParamName);
tab.Update();
return true;
}
@ -434,6 +581,9 @@ public partial class ModEditWindow
else if (!disabled && sizeWellDefined)
ret |= DrawShaderPackageNewParameter(tab);
if (tab.Shpk.Disassembled)
DrawShaderPackageMaterialDevkitExport(tab);
return ret;
}
@ -441,34 +591,38 @@ public partial class ModEditWindow
{
var ret = false;
if (!ImGui.CollapsingHeader("Shader Resources"))
if (!ImUtf8.CollapsingHeader("Shader Resources"u8))
return false;
ret |= DrawShaderPackageResourceArray("Constant Buffers", "type", true, tab.Shpk.Constants, disabled);
ret |= DrawShaderPackageResourceArray("Samplers", "type", false, tab.Shpk.Samplers, disabled);
ret |= DrawShaderPackageResourceArray("Unordered Access Views", "type", false, tab.Shpk.Uavs, disabled);
var hasFilters = tab.FilterPopCount != tab.FilterMaximumPopCount;
ret |= DrawShaderPackageResourceArray("Constant Buffers", "type", true, tab.Shpk.Constants, hasFilters, disabled);
ret |= DrawShaderPackageResourceArray("Samplers", "type", false, tab.Shpk.Samplers, hasFilters, disabled);
if (!tab.Shpk.IsLegacy)
ret |= DrawShaderPackageResourceArray("Textures", "type", false, tab.Shpk.Textures, hasFilters, disabled);
ret |= DrawShaderPackageResourceArray("Unordered Access Views", "type", false, tab.Shpk.Uavs, hasFilters, disabled);
return ret;
}
private static void DrawKeyArray(string arrayName, bool withId, IReadOnlyCollection<Key> keys)
private static void DrawKeyArray(ShpkTab tab, string arrayName, bool withId, IReadOnlyCollection<Key> keys)
{
if (keys.Count == 0)
return;
using var t = ImRaii.TreeNode(arrayName);
using var t = ImUtf8.TreeNode(arrayName);
if (!t)
return;
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
foreach (var (key, idx) in keys.WithIndex())
{
using var t2 = ImRaii.TreeNode(withId ? $"#{idx}: ID: 0x{key.Id:X8}" : $"#{idx}");
using var t2 = ImUtf8.TreeNode(withId ? $"#{idx}: {tab.TryResolveName(key.Id)} (0x{key.Id:X8})" : $"#{idx}");
if (t2)
{
ImRaii.TreeNode($"Default Value: 0x{key.DefaultValue:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose();
ImRaii.TreeNode($"Known Values: {string.Join(", ", Array.ConvertAll(key.Values, value => $"0x{value:X8}"))}",
ImUtf8.TreeNode($"Default Value: {tab.TryResolveName(key.DefaultValue)} (0x{key.DefaultValue:X8})",
ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose();
ImUtf8.TreeNode($"Known Values: {tab.NameSetToString(key.Values, true)}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet)
.Dispose();
}
}
}
@ -478,43 +632,55 @@ public partial class ModEditWindow
if (tab.Shpk.Nodes.Length <= 0)
return;
using var t = ImRaii.TreeNode($"Nodes ({tab.Shpk.Nodes.Length})###Nodes");
using var t = ImUtf8.TreeNode($"Nodes ({tab.Shpk.Nodes.Length})###Nodes");
if (!t)
return;
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
foreach (var (node, idx) in tab.Shpk.Nodes.WithIndex())
{
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
using var t2 = ImRaii.TreeNode($"#{idx:D4}: Selector: 0x{node.Selector:X8}");
if (!tab.IsFilterMatch(node))
continue;
using var t2 = ImUtf8.TreeNode($"#{idx:D4}: Selector: 0x{node.Selector:X8}");
if (!t2)
continue;
foreach (var (key, keyIdx) in node.SystemKeys.WithIndex())
{
ImRaii.TreeNode($"System Key 0x{tab.Shpk.SystemKeys[keyIdx].Id:X8} = 0x{key:X8}",
ImUtf8.TreeNode(
$"System Key {tab.TryResolveName(tab.Shpk.SystemKeys[keyIdx].Id)} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.SystemValues![keyIdx])} }}",
ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose();
}
foreach (var (key, keyIdx) in node.SceneKeys.WithIndex())
{
ImRaii.TreeNode($"Scene Key 0x{tab.Shpk.SceneKeys[keyIdx].Id:X8} = 0x{key:X8}",
ImUtf8.TreeNode(
$"Scene Key {tab.TryResolveName(tab.Shpk.SceneKeys[keyIdx].Id)} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.SceneValues![keyIdx])} }}",
ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose();
}
foreach (var (key, keyIdx) in node.MaterialKeys.WithIndex())
{
ImRaii.TreeNode($"Material Key 0x{tab.Shpk.MaterialKeys[keyIdx].Id:X8} = 0x{key:X8}",
ImUtf8.TreeNode(
$"Material Key {tab.TryResolveName(tab.Shpk.MaterialKeys[keyIdx].Id)} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.MaterialValues![keyIdx])} }}",
ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose();
}
foreach (var (key, keyIdx) in node.SubViewKeys.WithIndex())
ImRaii.TreeNode($"Sub-View Key #{keyIdx} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose();
{
ImUtf8.TreeNode(
$"Sub-View Key #{keyIdx} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.SubViewValues![keyIdx])} }}",
ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose();
}
ImRaii.TreeNode($"Pass Indices: {string.Join(' ', node.PassIndices.Select(c => $"{c:X2}"))}",
ImUtf8.TreeNode($"Pass Indices: {string.Join(' ', node.PassIndices.Select(c => $"{c:X2}"))}",
ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose();
foreach (var (pass, passIdx) in node.Passes.WithIndex())
{
ImRaii.TreeNode($"Pass #{passIdx}: ID: 0x{pass.Id:X8}, Vertex Shader #{pass.VertexShader}, Pixel Shader #{pass.PixelShader}",
ImUtf8.TreeNode(
$"Pass #{passIdx}: ID: {tab.TryResolveName(pass.Id)}, Vertex Shader #{pass.VertexShader}, Pixel Shader #{pass.PixelShader}",
ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet)
.Dispose();
}
@ -523,22 +689,22 @@ public partial class ModEditWindow
private static void DrawShaderPackageSelection(ShpkTab tab)
{
if (!ImGui.CollapsingHeader("Shader Selection"))
if (!ImUtf8.CollapsingHeader("Shader Selection"u8))
return;
DrawKeyArray("System Keys", true, tab.Shpk.SystemKeys);
DrawKeyArray("Scene Keys", true, tab.Shpk.SceneKeys);
DrawKeyArray("Material Keys", true, tab.Shpk.MaterialKeys);
DrawKeyArray("Sub-View Keys", false, tab.Shpk.SubViewKeys);
DrawKeyArray(tab, "System Keys", true, tab.Shpk.SystemKeys);
DrawKeyArray(tab, "Scene Keys", true, tab.Shpk.SceneKeys);
DrawKeyArray(tab, "Material Keys", true, tab.Shpk.MaterialKeys);
DrawKeyArray(tab, "Sub-View Keys", false, tab.Shpk.SubViewKeys);
DrawShaderPackageNodes(tab);
using var t = ImRaii.TreeNode($"Node Selectors ({tab.Shpk.NodeSelectors.Count})###NodeSelectors");
using var t = ImUtf8.TreeNode($"Node Selectors ({tab.Shpk.NodeSelectors.Count})###NodeSelectors");
if (t)
{
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
foreach (var selector in tab.Shpk.NodeSelectors)
{
ImRaii.TreeNode($"#{selector.Value:D4}: Selector: 0x{selector.Key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet)
ImUtf8.TreeNode($"#{selector.Value:D4}: Selector: 0x{selector.Key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet)
.Dispose();
}
}
@ -546,25 +712,27 @@ public partial class ModEditWindow
private static void DrawOtherShaderPackageDetails(ShpkTab tab)
{
if (!ImGui.CollapsingHeader("Further Content"))
if (!ImUtf8.CollapsingHeader("Further Content"u8))
return;
ImRaii.TreeNode($"Version: 0x{tab.Shpk.Version:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose();
ImUtf8.TreeNode($"Version: 0x{tab.Shpk.Version:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose();
if (tab.Shpk.AdditionalData.Length > 0)
{
using var t = ImRaii.TreeNode($"Additional Data (Size: {tab.Shpk.AdditionalData.Length})###AdditionalData");
using var t = ImUtf8.TreeNode($"Additional Data (Size: {tab.Shpk.AdditionalData.Length})###AdditionalData");
if (t)
Widget.DrawHexViewer(tab.Shpk.AdditionalData);
}
}
private static string UsedComponentString(bool withSize, in Resource resource)
private static string UsedComponentString(bool withSize, bool filtered, in Resource resource)
{
var sb = new StringBuilder(256);
var used = filtered ? resource.FilteredUsed : resource.Used;
var usedDynamically = filtered ? resource.FilteredUsedDynamically : resource.UsedDynamically;
var sb = new StringBuilder(256);
if (withSize)
{
foreach (var (components, i) in (resource.Used ?? Array.Empty<DisassembledShader.VectorComponents>()).WithIndex())
foreach (var (components, i) in (used ?? Array.Empty<DisassembledShader.VectorComponents>()).WithIndex())
{
switch (components)
{
@ -582,7 +750,7 @@ public partial class ModEditWindow
}
}
switch (resource.UsedDynamically ?? 0)
switch (usedDynamically ?? 0)
{
case 0: break;
case DisassembledShader.VectorComponents.All:
@ -590,7 +758,7 @@ public partial class ModEditWindow
break;
default:
sb.Append("[*].");
foreach (var c in resource.UsedDynamically!.Value.ToString().Where(char.IsUpper))
foreach (var c in usedDynamically!.Value.ToString().Where(char.IsUpper))
sb.Append(char.ToLower(c));
sb.Append(", ");
@ -599,7 +767,7 @@ public partial class ModEditWindow
}
else
{
var components = (resource.Used is { Length: > 0 } ? resource.Used[0] : 0) | (resource.UsedDynamically ?? 0);
var components = (used is { Length: > 0 } ? used[0] : 0) | (usedDynamically ?? 0);
if ((components & DisassembledShader.VectorComponents.X) != 0)
sb.Append("Red, ");

View file

@ -1,9 +1,12 @@
using Dalamud.Utility;
using Lumina.Misc;
using Newtonsoft.Json.Linq;
using OtterGui;
using Penumbra.GameData.Data;
using OtterGui.Classes;
using Penumbra.GameData.Files;
using Penumbra.GameData.Files.ShaderStructs;
using Penumbra.GameData.Interop;
using Penumbra.GameData.Structs;
using Penumbra.UI.AdvancedWindow.Materials;
namespace Penumbra.UI.AdvancedWindow;
@ -12,18 +15,27 @@ public partial class ModEditWindow
private class ShpkTab : IWritable
{
public readonly ShpkFile Shpk;
public readonly string FilePath;
public string NewMaterialParamName = string.Empty;
public uint NewMaterialParamId = Crc32.Get(string.Empty, 0xFFFFFFFFu);
public short NewMaterialParamStart;
public short NewMaterialParamEnd;
public Name NewMaterialParamName = string.Empty;
public short NewMaterialParamStart;
public short NewMaterialParamEnd;
public readonly SharedSet<uint, uint>[] FilterSystemValues;
public readonly SharedSet<uint, uint>[] FilterSceneValues;
public readonly SharedSet<uint, uint>[] FilterMaterialValues;
public readonly SharedSet<uint, uint>[] FilterSubViewValues;
public SharedSet<uint, uint> FilterPasses;
public readonly int FilterMaximumPopCount;
public int FilterPopCount;
public readonly FileDialogService FileDialog;
public readonly string Header;
public readonly string Extension;
public ShpkTab(FileDialogService fileDialog, byte[] bytes)
public ShpkTab(FileDialogService fileDialog, byte[] bytes, string filePath)
{
FileDialog = fileDialog;
try
@ -35,6 +47,8 @@ public partial class ModEditWindow
Shpk = new ShpkFile(bytes, false);
}
FilePath = filePath;
Header = $"Shader Package for DirectX {(int)Shpk.DirectXVersion}";
Extension = Shpk.DirectXVersion switch
{
@ -42,15 +56,36 @@ public partial class ModEditWindow
ShpkFile.DxVersion.DirectX11 => ".dxbc",
_ => throw new NotImplementedException(),
};
FilterSystemValues = Array.ConvertAll(Shpk.SystemKeys, key => key.Values.FullSet());
FilterSceneValues = Array.ConvertAll(Shpk.SceneKeys, key => key.Values.FullSet());
FilterMaterialValues = Array.ConvertAll(Shpk.MaterialKeys, key => key.Values.FullSet());
FilterSubViewValues = Array.ConvertAll(Shpk.SubViewKeys, key => key.Values.FullSet());
FilterPasses = Shpk.Passes.FullSet();
FilterMaximumPopCount = FilterPasses.Count;
foreach (var key in Shpk.SystemKeys)
FilterMaximumPopCount += key.Values.Count;
foreach (var key in Shpk.SceneKeys)
FilterMaximumPopCount += key.Values.Count;
foreach (var key in Shpk.MaterialKeys)
FilterMaximumPopCount += key.Values.Count;
foreach (var key in Shpk.SubViewKeys)
FilterMaximumPopCount += key.Values.Count;
FilterPopCount = FilterMaximumPopCount;
UpdateNameCache();
Shpk.UpdateFilteredUsed(IsFilterMatch);
Update();
}
[Flags]
public enum ColorType : byte
{
Unused = 0,
Used = 1,
Continuation = 2,
FilteredUsed = 2,
Continuation = 4,
}
public (string Name, string Tooltip, short Index, ColorType Color)[,] Matrix = null!;
@ -58,10 +93,89 @@ public partial class ModEditWindow
public readonly HashSet<uint> UsedIds = new(16);
public readonly List<(string Name, short Index)> Orphans = new(16);
private readonly Dictionary<uint, Name> _nameCache = [];
private readonly Dictionary<SharedSet<uint, uint>, string> _nameSetCache = [];
private readonly Dictionary<SharedSet<uint, uint>, string> _nameSetWithIdsCache = [];
public void AddNameToCache(Name name)
{
if (name.Value != null)
_nameCache.TryAdd(name.Crc32, name);
_nameSetCache.Clear();
_nameSetWithIdsCache.Clear();
}
private void UpdateNameCache()
{
CollectResourceNames(_nameCache, Shpk.Constants);
CollectResourceNames(_nameCache, Shpk.Samplers);
CollectResourceNames(_nameCache, Shpk.Textures);
CollectResourceNames(_nameCache, Shpk.Uavs);
CollectKeyNames(_nameCache, Shpk.SystemKeys);
CollectKeyNames(_nameCache, Shpk.SceneKeys);
CollectKeyNames(_nameCache, Shpk.MaterialKeys);
CollectKeyNames(_nameCache, Shpk.SubViewKeys);
_nameSetCache.Clear();
_nameSetWithIdsCache.Clear();
return;
static void CollectKeyNames(Dictionary<uint, Name> nameCache, ShpkFile.Key[] keys)
{
foreach (var key in keys)
{
var keyName = nameCache.TryResolve(Names.KnownNames, key.Id);
var valueNames = keyName.WithKnownSuffixes();
foreach (var value in key.Values)
{
var valueName = valueNames.TryResolve(value);
if (valueName.Value != null)
nameCache.TryAdd(value, valueName);
}
}
}
static void CollectResourceNames(Dictionary<uint, Name> nameCache, ShpkFile.Resource[] resources)
{
foreach (var resource in resources)
nameCache.TryAdd(resource.Id, resource.Name);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Name TryResolveName(uint crc32)
=> _nameCache.TryResolve(Names.KnownNames, crc32);
public string NameSetToString(SharedSet<uint, uint> nameSet, bool withIds = false)
{
var cache = withIds ? _nameSetWithIdsCache : _nameSetCache;
if (cache.TryGetValue(nameSet, out var nameSetStr))
return nameSetStr;
if (withIds)
nameSetStr = string.Join(", ", nameSet.Select(id => $"{TryResolveName(id)} (0x{id:X8})"));
else
nameSetStr = string.Join(", ", nameSet.Select(TryResolveName));
cache.Add(nameSet, nameSetStr);
return nameSetStr;
}
public void UpdateFilteredUsed()
{
Shpk.UpdateFilteredUsed(IsFilterMatch);
var materialParams = Shpk.GetConstantById(ShpkFile.MaterialParamsConstantId);
UpdateColors(materialParams);
}
public void Update()
{
var materialParams = Shpk.GetConstantById(ShpkFile.MaterialParamsConstantId);
var numParameters = ((Shpk.MaterialParamsSize + 0xFu) & ~0xFu) >> 4;
var defaults = Shpk.MaterialParamsDefaults != null ? (ReadOnlySpan<byte>)Shpk.MaterialParamsDefaults : [];
var defaultFloats = MemoryMarshal.Cast<byte, float>(defaults);
Matrix = new (string Name, string Tooltip, short Index, ColorType Color)[numParameters, 4];
MalformedParameters.Clear();
@ -75,14 +189,15 @@ public partial class ModEditWindow
var jEnd = ((param.ByteOffset + param.ByteSize - 1) >> 2) & 3;
if ((param.ByteOffset & 0x3) != 0 || (param.ByteSize & 0x3) != 0)
{
MalformedParameters.Add($"ID: 0x{param.Id:X8}, offset: 0x{param.ByteOffset:X4}, size: 0x{param.ByteSize:X4}");
MalformedParameters.Add(
$"ID: {TryResolveName(param.Id)} (0x{param.Id:X8}), offset: 0x{param.ByteOffset:X4}, size: 0x{param.ByteSize:X4}");
continue;
}
if (iEnd >= numParameters)
{
MalformedParameters.Add(
$"{MaterialParamRangeName(materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2)} (ID: 0x{param.Id:X8})");
$"{MtrlTab.MaterialParamRangeName(materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2)} ({TryResolveName(param.Id)}, 0x{param.Id:X8})");
continue;
}
@ -91,9 +206,13 @@ public partial class ModEditWindow
var end = i == iEnd ? jEnd : 3;
for (var j = i == iStart ? jStart : 0; j <= end; ++j)
{
var component = (i << 2) | j;
var tt =
$"{MaterialParamRangeName(materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2).Item1} (ID: 0x{param.Id:X8})";
Matrix[i, j] = ($"0x{param.Id:X8}", tt, (short)idx, 0);
$"{MtrlTab.MaterialParamRangeName(materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2).Item1} ({TryResolveName(param.Id)}, 0x{param.Id:X8})";
if (component < defaultFloats.Length)
tt +=
$"\n\nDefault value: {defaultFloats[component]} ({defaults[component << 2]:X2} {defaults[(component << 2) | 1]:X2} {defaults[(component << 2) | 2]:X2} {defaults[(component << 2) | 3]:X2})";
Matrix[i, j] = (TryResolveName(param.Id).ToString(), tt, (short)idx, 0);
}
}
}
@ -151,7 +270,8 @@ public partial class ModEditWindow
if (oldStart == linear)
newMaterialParamStart = (short)Orphans.Count;
Orphans.Add(($"{materialParams?.Name ?? string.Empty}{MaterialParamName(false, linear)}", linear));
Orphans.Add(($"{materialParams?.Name ?? ShpkFile.MaterialParamsConstantName}{MtrlTab.MaterialParamName(false, linear)}",
linear));
}
}
@ -168,11 +288,15 @@ public partial class ModEditWindow
{
var usedComponents = (materialParams?.Used?[i] ?? DisassembledShader.VectorComponents.All)
| (materialParams?.UsedDynamically ?? 0);
var filteredUsedComponents = (materialParams?.FilteredUsed?[i] ?? DisassembledShader.VectorComponents.All)
| (materialParams?.FilteredUsedDynamically ?? 0);
for (var j = 0; j < 4; ++j)
{
var color = ((byte)usedComponents & (1 << j)) != 0
? ColorType.Used
: 0;
ColorType color = 0;
if (((byte)usedComponents & (1 << j)) != 0)
color |= ColorType.Used;
if (((byte)filteredUsedComponents & (1 << j)) != 0)
color |= ColorType.FilteredUsed;
if (Matrix[i, j].Index == lastIndex || Matrix[i, j].Index < 0)
color |= ColorType.Continuation;
@ -182,6 +306,137 @@ public partial class ModEditWindow
}
}
public bool IsFilterMatch(ShpkFile.Shader shader)
{
if (!FilterPasses.Overlaps(shader.Passes))
return false;
for (var i = 0; i < shader.SystemValues!.Length; ++i)
{
if (!FilterSystemValues[i].Overlaps(shader.SystemValues[i]))
return false;
}
for (var i = 0; i < shader.SceneValues!.Length; ++i)
{
if (!FilterSceneValues[i].Overlaps(shader.SceneValues[i]))
return false;
}
for (var i = 0; i < shader.MaterialValues!.Length; ++i)
{
if (!FilterMaterialValues[i].Overlaps(shader.MaterialValues[i]))
return false;
}
for (var i = 0; i < shader.SubViewValues!.Length; ++i)
{
if (!FilterSubViewValues[i].Overlaps(shader.SubViewValues[i]))
return false;
}
return true;
}
public bool IsFilterMatch(ShpkFile.Node node)
{
if (!node.Passes.Any(pass => FilterPasses.Contains(pass.Id)))
return false;
for (var i = 0; i < node.SystemValues!.Length; ++i)
{
if (!FilterSystemValues[i].Overlaps(node.SystemValues[i]))
return false;
}
for (var i = 0; i < node.SceneValues!.Length; ++i)
{
if (!FilterSceneValues[i].Overlaps(node.SceneValues[i]))
return false;
}
for (var i = 0; i < node.MaterialValues!.Length; ++i)
{
if (!FilterMaterialValues[i].Overlaps(node.MaterialValues[i]))
return false;
}
for (var i = 0; i < node.SubViewValues!.Length; ++i)
{
if (!FilterSubViewValues[i].Overlaps(node.SubViewValues[i]))
return false;
}
return true;
}
/// <summary>
/// Generates a minimal material dev-kit file for the given shader package.
///
/// This file currently only hides globally unused material constants.
/// </summary>
public JObject ExportDevkit()
{
var devkit = new JObject();
var maybeMaterialParameter = Shpk.GetConstantById(ShpkFile.MaterialParamsConstantId);
if (maybeMaterialParameter.HasValue)
{
var materialParameter = maybeMaterialParameter.Value;
var materialParameterUsage = new IndexSet(materialParameter.Size << 2, true);
var used = materialParameter.Used ?? [];
var usedDynamically = materialParameter.UsedDynamically ?? 0;
for (var i = 0; i < used.Length; ++i)
{
for (var j = 0; j < 4; ++j)
{
if (!(used[i] | usedDynamically).HasFlag((DisassembledShader.VectorComponents)(1 << j)))
materialParameterUsage[(i << 2) | j] = false;
}
}
var dkConstants = new JObject();
foreach (var param in Shpk.MaterialParams)
{
// Don't handle misaligned parameters.
if ((param.ByteOffset & 0x3) != 0 || (param.ByteSize & 0x3) != 0)
continue;
var start = param.ByteOffset >> 2;
var length = param.ByteSize >> 2;
// If the parameter is fully used, don't include it.
if (!materialParameterUsage.Indices(start, length, true).Any())
continue;
var unusedSlices = new JArray();
if (materialParameterUsage.Indices(start, length).Any())
foreach (var (rgStart, rgEnd) in materialParameterUsage.Ranges(start, length, true))
{
unusedSlices.Add(new JObject
{
["Type"] = "Hidden",
["Offset"] = rgStart,
["Length"] = rgEnd - rgStart,
});
}
else
unusedSlices.Add(new JObject
{
["Type"] = "Hidden",
});
dkConstants[param.Id.ToString()] = unusedSlices;
}
devkit["Constants"] = dkConstants;
}
return devkit;
}
public bool Valid
=> Shpk.Valid;

View file

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

View file

@ -6,7 +6,6 @@ using OtterGui;
using Penumbra.Interop.ResourceTree;
using Penumbra.UI.Classes;
using Penumbra.String;
using Penumbra.UI.Tabs;
namespace Penumbra.UI.AdvancedWindow;
@ -245,10 +244,7 @@ public class ResourceTreeViewer
if (visibility == NodeVisibility.Hidden)
continue;
var textColor = ImGui.GetColorU32(ImGuiCol.Text);
var textColorInternal = (textColor & 0x00FFFFFFu) | ((textColor & 0xFE000000u) >> 1); // Half opacity
using var mutedColor = ImRaii.PushColor(ImGuiCol.Text, textColorInternal, resourceNode.Internal);
using var mutedColor = ImRaii.PushColor(ImGuiCol.Text, ImGuiUtil.HalfTransparentText(), resourceNode.Internal);
var filterIcon = resourceNode.IconFlag != 0 ? resourceNode.IconFlag : parentFilterIconFlag;

View file

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

View file

@ -42,6 +42,9 @@ using ImGuiClip = OtterGui.ImGuiClip;
using Penumbra.Api.IpcTester;
using Penumbra.Interop.Hooks.PostProcessing;
using Penumbra.Interop.Hooks.ResourceLoading;
using Penumbra.GameData.Files.StainMapStructs;
using Penumbra.UI.AdvancedWindow;
using Penumbra.UI.AdvancedWindow.Materials;
namespace Penumbra.UI.Tabs.Debug;
@ -697,32 +700,48 @@ public class DebugTab : Window, ITab, IUiService
if (!mainTree)
return;
foreach (var (key, data) in _stains.StmFile.Entries)
using (var legacyTree = TreeNode("stainingtemplate.stm"))
{
if (legacyTree)
DrawStainTemplatesFile(_stains.LegacyStmFile);
}
using (var gudTree = TreeNode("stainingtemplate_gud.stm"))
{
if (gudTree)
DrawStainTemplatesFile(_stains.GudStmFile);
}
}
private static void DrawStainTemplatesFile<TDyePack>(StmFile<TDyePack> stmFile) where TDyePack : unmanaged, IDyePack
{
foreach (var (key, data) in stmFile.Entries)
{
using var tree = TreeNode($"Template {key}");
if (!tree)
continue;
using var table = Table("##table", 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
using var table = Table("##table", data.Colors.Length + data.Scalars.Length, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
if (!table)
continue;
for (var i = 0; i < StmFile.StainingTemplateEntry.NumElements; ++i)
for (var i = 0; i < StmFile<TDyePack>.StainingTemplateEntry.NumElements; ++i)
{
var (r, g, b) = data.DiffuseEntries[i];
ImGuiUtil.DrawTableColumn($"{r:F6} | {g:F6} | {b:F6}");
foreach (var list in data.Colors)
{
var color = list[i];
ImGui.TableNextColumn();
var frame = new Vector2(ImGui.GetTextLineHeight());
ImGui.ColorButton("###color", new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)color), 1), 0, frame);
ImGui.SameLine();
ImGui.TextUnformatted($"{color.Red:F6} | {color.Green:F6} | {color.Blue:F6}");
}
(r, g, b) = data.SpecularEntries[i];
ImGuiUtil.DrawTableColumn($"{r:F6} | {g:F6} | {b:F6}");
(r, g, b) = data.EmissiveEntries[i];
ImGuiUtil.DrawTableColumn($"{r:F6} | {g:F6} | {b:F6}");
var a = data.SpecularPowerEntries[i];
ImGuiUtil.DrawTableColumn($"{a:F6}");
a = data.GlossEntries[i];
ImGuiUtil.DrawTableColumn($"{a:F6}");
foreach (var list in data.Scalars)
{
var scalar = list[i];
ImGuiUtil.DrawTableColumn($"{scalar:F6}");
}
}
}
}

View file

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

View file

@ -2,6 +2,7 @@ using Dalamud.Interface;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin;
using OtterGui.Services;
using Penumbra.Interop.Services;
using Penumbra.UI.AdvancedWindow;
using Penumbra.UI.Knowledge;
using Penumbra.UI.Tabs.Debug;
@ -10,23 +11,25 @@ namespace Penumbra.UI;
public class PenumbraWindowSystem : IDisposable, IUiService
{
private readonly IUiBuilder _uiBuilder;
private readonly WindowSystem _windowSystem;
private readonly FileDialogService _fileDialog;
public readonly ConfigWindow Window;
public readonly PenumbraChangelog Changelog;
public readonly KnowledgeWindow KnowledgeWindow;
private readonly IUiBuilder _uiBuilder;
private readonly WindowSystem _windowSystem;
private readonly FileDialogService _fileDialog;
private readonly TextureArraySlicer _textureArraySlicer;
public readonly ConfigWindow Window;
public readonly PenumbraChangelog Changelog;
public readonly KnowledgeWindow KnowledgeWindow;
public PenumbraWindowSystem(IDalamudPluginInterface pi, Configuration config, PenumbraChangelog changelog, ConfigWindow window,
LaunchButton _, ModEditWindow editWindow, FileDialogService fileDialog, ImportPopup importPopup, DebugTab debugTab,
KnowledgeWindow knowledgeWindow)
KnowledgeWindow knowledgeWindow, TextureArraySlicer textureArraySlicer)
{
_uiBuilder = pi.UiBuilder;
_fileDialog = fileDialog;
KnowledgeWindow = knowledgeWindow;
Changelog = changelog;
Window = window;
_windowSystem = new WindowSystem("Penumbra");
_uiBuilder = pi.UiBuilder;
_fileDialog = fileDialog;
_textureArraySlicer = textureArraySlicer;
KnowledgeWindow = knowledgeWindow;
Changelog = changelog;
Window = window;
_windowSystem = new WindowSystem("Penumbra");
_windowSystem.AddWindow(changelog.Changelog);
_windowSystem.AddWindow(window);
_windowSystem.AddWindow(editWindow);
@ -37,6 +40,7 @@ public class PenumbraWindowSystem : IDisposable, IUiService
_uiBuilder.OpenConfigUi += Window.OpenSettings;
_uiBuilder.Draw += _windowSystem.Draw;
_uiBuilder.Draw += _fileDialog.Draw;
_uiBuilder.Draw += _textureArraySlicer.Tick;
_uiBuilder.DisableGposeUiHide = !config.HideUiInGPose;
_uiBuilder.DisableCutsceneUiHide = !config.HideUiInCutscenes;
_uiBuilder.DisableUserUiHide = !config.HideUiWhenUiHidden;
@ -51,5 +55,6 @@ public class PenumbraWindowSystem : IDisposable, IUiService
_uiBuilder.OpenConfigUi -= Window.OpenSettings;
_uiBuilder.Draw -= _windowSystem.Draw;
_uiBuilder.Draw -= _fileDialog.Draw;
_uiBuilder.Draw -= _textureArraySlicer.Tick;
}
}