mirror of
https://github.com/xivdev/Penumbra.git
synced 2026-02-06 07:54:36 +01:00
Merge branch 'dtme'
This commit is contained in:
commit
f2094c2c58
38 changed files with 4540 additions and 2496 deletions
|
|
@ -107,6 +107,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService
|
|||
public bool AlwaysOpenDefaultImport { get; set; } = false;
|
||||
public bool KeepDefaultMetaChanges { get; set; } = false;
|
||||
public string DefaultModAuthor { get; set; } = DefaultTexToolsData.Author;
|
||||
public bool EditRawTileTransforms { get; set; } = false;
|
||||
|
||||
public Dictionary<ColorId, uint> Colors { get; set; }
|
||||
= Enum.GetValues<ColorId>().ToDictionary(c => c, c => c.Data().DefaultColor);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using Penumbra.GameData.Interop;
|
||||
using Penumbra.Interop.SafeHandles;
|
||||
|
||||
|
|
@ -7,10 +8,6 @@ namespace Penumbra.Interop.MaterialPreview;
|
|||
|
||||
public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase
|
||||
{
|
||||
public const int TextureWidth = 4;
|
||||
public const int TextureHeight = GameData.Files.MaterialStructs.LegacyColorTable.NumUsedRows;
|
||||
public const int TextureLength = TextureWidth * TextureHeight * 4;
|
||||
|
||||
private readonly IFramework _framework;
|
||||
|
||||
private readonly Texture** _colorTableTexture;
|
||||
|
|
@ -18,6 +15,9 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase
|
|||
|
||||
private bool _updatePending;
|
||||
|
||||
public int Width { get; }
|
||||
public int Height { get; }
|
||||
|
||||
public Half[] ColorTable { get; }
|
||||
|
||||
public LiveColorTablePreviewer(ObjectManager objects, IFramework framework, MaterialInfo materialInfo)
|
||||
|
|
@ -33,18 +33,24 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase
|
|||
if (colorSetTextures == null)
|
||||
throw new InvalidOperationException("Draw object doesn't have color table textures");
|
||||
|
||||
_colorTableTexture = colorSetTextures + (MaterialInfo.ModelSlot * 4 + MaterialInfo.MaterialSlot);
|
||||
_colorTableTexture = colorSetTextures + (MaterialInfo.ModelSlot * CharacterBase.MaterialsPerSlot + MaterialInfo.MaterialSlot);
|
||||
|
||||
|
||||
_originalColorTableTexture = new SafeTextureHandle(*_colorTableTexture, true);
|
||||
if (_originalColorTableTexture == null)
|
||||
throw new InvalidOperationException("Material doesn't have a color table");
|
||||
|
||||
ColorTable = new Half[TextureLength];
|
||||
Width = (int)_originalColorTableTexture.Texture->Width;
|
||||
Height = (int)_originalColorTableTexture.Texture->Height;
|
||||
ColorTable = new Half[Width * Height * 4];
|
||||
_updatePending = true;
|
||||
|
||||
framework.Update += OnFrameworkUpdate;
|
||||
}
|
||||
|
||||
public Span<Half> GetColorRow(int i)
|
||||
=> ColorTable.AsSpan().Slice(Width * 4 * i, Width * 4);
|
||||
|
||||
protected override void Clear(bool disposing, bool reset)
|
||||
{
|
||||
_framework.Update -= OnFrameworkUpdate;
|
||||
|
|
@ -74,8 +80,8 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase
|
|||
return;
|
||||
|
||||
var textureSize = stackalloc int[2];
|
||||
textureSize[0] = TextureWidth;
|
||||
textureSize[1] = TextureHeight;
|
||||
textureSize[0] = Width;
|
||||
textureSize[1] = Height;
|
||||
|
||||
using var texture =
|
||||
new SafeTextureHandle(Device.Instance()->CreateTexture2D(textureSize, 1, 0x2460, 0x80000804, 7), false);
|
||||
|
|
@ -104,6 +110,6 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase
|
|||
if (colorSetTextures == null)
|
||||
return false;
|
||||
|
||||
return _colorTableTexture == colorSetTextures + (MaterialInfo.ModelSlot * 4 + MaterialInfo.MaterialSlot);
|
||||
return _colorTableTexture == colorSetTextures + (MaterialInfo.ModelSlot * CharacterBase.MaterialsPerSlot + MaterialInfo.MaterialSlot);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase
|
|||
{
|
||||
private readonly ShaderPackage* _shaderPackage;
|
||||
|
||||
private readonly uint _originalShPkFlags;
|
||||
private readonly float[] _originalMaterialParameter;
|
||||
private readonly uint[] _originalSamplerFlags;
|
||||
private readonly uint _originalShPkFlags;
|
||||
private readonly byte[] _originalMaterialParameter;
|
||||
private readonly uint[] _originalSamplerFlags;
|
||||
|
||||
public LiveMaterialPreviewer(ObjectManager objects, MaterialInfo materialInfo)
|
||||
: base(objects, materialInfo)
|
||||
|
|
@ -28,7 +28,7 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase
|
|||
|
||||
_originalShPkFlags = Material->ShaderFlags;
|
||||
|
||||
_originalMaterialParameter = Material->MaterialParameterCBuffer->TryGetBuffer().ToArray();
|
||||
_originalMaterialParameter = Material->MaterialParameterCBuffer->TryGetBuffer<byte>().ToArray();
|
||||
|
||||
_originalSamplerFlags = new uint[Material->TextureCount];
|
||||
for (var i = 0; i < _originalSamplerFlags.Length; ++i)
|
||||
|
|
@ -43,7 +43,7 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase
|
|||
return;
|
||||
|
||||
Material->ShaderFlags = _originalShPkFlags;
|
||||
var materialParameter = Material->MaterialParameterCBuffer->TryGetBuffer();
|
||||
var materialParameter = Material->MaterialParameterCBuffer->TryGetBuffer<byte>();
|
||||
if (!materialParameter.IsEmpty)
|
||||
_originalMaterialParameter.AsSpan().CopyTo(materialParameter);
|
||||
|
||||
|
|
@ -59,7 +59,7 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase
|
|||
Material->ShaderFlags = shPkFlags;
|
||||
}
|
||||
|
||||
public void SetMaterialParameter(uint parameterCrc, Index offset, Span<float> value)
|
||||
public void SetMaterialParameter(uint parameterCrc, Index offset, ReadOnlySpan<byte> value)
|
||||
{
|
||||
if (!CheckValidity())
|
||||
return;
|
||||
|
|
@ -68,7 +68,7 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase
|
|||
if (constantBuffer == null)
|
||||
return;
|
||||
|
||||
var buffer = constantBuffer->TryGetBuffer();
|
||||
var buffer = constantBuffer->TryGetBuffer<byte>();
|
||||
if (buffer.IsEmpty)
|
||||
return;
|
||||
|
||||
|
|
@ -78,12 +78,10 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase
|
|||
if (parameter.CRC != parameterCrc)
|
||||
continue;
|
||||
|
||||
if ((parameter.Offset & 0x3) != 0
|
||||
|| (parameter.Size & 0x3) != 0
|
||||
|| (parameter.Offset + parameter.Size) >> 2 > buffer.Length)
|
||||
if (parameter.Offset + parameter.Size > buffer.Length)
|
||||
return;
|
||||
|
||||
value.TryCopyTo(buffer.Slice(parameter.Offset >> 2, parameter.Size >> 2)[offset..]);
|
||||
value.TryCopyTo(buffer.Slice(parameter.Offset, parameter.Size)[offset..]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
119
Penumbra/Interop/Services/TextureArraySlicer.cs
Normal file
119
Penumbra/Interop/Services/TextureArraySlicer.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
71
Penumbra/UI/AdvancedWindow/Materials/ConstantEditors.cs
Normal file
71
Penumbra/UI/AdvancedWindow/Materials/ConstantEditors.cs
Normal 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);
|
||||
}
|
||||
177
Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs
Normal file
177
Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
624
Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs
Normal file
624
Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
540
Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs
Normal file
540
Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs
Normal 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);
|
||||
}
|
||||
278
Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs
Normal file
278
Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
252
Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Devkit.cs
Normal file
252
Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Devkit.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
380
Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs
Normal file
380
Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
275
Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs
Normal file
275
Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
507
Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs
Normal file
507
Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
279
Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs
Normal file
279
Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
199
Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs
Normal file
199
Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
using Dalamud.Plugin.Services;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Text;
|
||||
using OtterGui.Widgets;
|
||||
using Penumbra.GameData.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();
|
||||
}
|
||||
}
|
||||
25
Penumbra/UI/AdvancedWindow/Materials/MtrlTabFactory.cs
Normal file
25
Penumbra/UI/AdvancedWindow/Materials/MtrlTabFactory.cs
Normal 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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -1,247 +0,0 @@
|
|||
using ImGuiNET;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui;
|
||||
using Penumbra.GameData;
|
||||
|
||||
namespace Penumbra.UI.AdvancedWindow;
|
||||
|
||||
public partial class ModEditWindow
|
||||
{
|
||||
private interface IConstantEditor
|
||||
{
|
||||
bool Draw(Span<float> values, bool disabled);
|
||||
}
|
||||
|
||||
private sealed class FloatConstantEditor : IConstantEditor
|
||||
{
|
||||
public static readonly FloatConstantEditor Default = new(null, null, 0.1f, 0.0f, 1.0f, 0.0f, 3, string.Empty);
|
||||
|
||||
private readonly float? _minimum;
|
||||
private readonly float? _maximum;
|
||||
private readonly float _speed;
|
||||
private readonly float _relativeSpeed;
|
||||
private readonly float _factor;
|
||||
private readonly float _bias;
|
||||
private readonly string _format;
|
||||
|
||||
public FloatConstantEditor(float? minimum, float? maximum, float speed, float relativeSpeed, float factor, float bias, byte precision,
|
||||
string unit)
|
||||
{
|
||||
_minimum = minimum;
|
||||
_maximum = maximum;
|
||||
_speed = speed;
|
||||
_relativeSpeed = relativeSpeed;
|
||||
_factor = factor;
|
||||
_bias = bias;
|
||||
_format = $"%.{Math.Min(precision, (byte)9)}f";
|
||||
if (unit.Length > 0)
|
||||
_format = $"{_format} {unit.Replace("%", "%%")}";
|
||||
}
|
||||
|
||||
public bool Draw(Span<float> values, bool disabled)
|
||||
{
|
||||
var spacing = ImGui.GetStyle().ItemInnerSpacing.X;
|
||||
var fieldWidth = (ImGui.CalcItemWidth() - (values.Length - 1) * spacing) / values.Length;
|
||||
|
||||
var ret = false;
|
||||
|
||||
// Not using DragScalarN because of _relativeSpeed and other points of lost flexibility.
|
||||
for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx)
|
||||
{
|
||||
if (valueIdx > 0)
|
||||
ImGui.SameLine(0.0f, spacing);
|
||||
|
||||
ImGui.SetNextItemWidth(MathF.Round(fieldWidth * (valueIdx + 1)) - MathF.Round(fieldWidth * valueIdx));
|
||||
|
||||
var value = (values[valueIdx] - _bias) / _factor;
|
||||
if (disabled)
|
||||
{
|
||||
ImGui.DragFloat($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), value, value, _format);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ImGui.DragFloat($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), _minimum ?? 0.0f,
|
||||
_maximum ?? 0.0f, _format))
|
||||
{
|
||||
values[valueIdx] = Clamp(value) * _factor + _bias;
|
||||
ret = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private float Clamp(float value)
|
||||
=> Math.Clamp(value, _minimum ?? float.NegativeInfinity, _maximum ?? float.PositiveInfinity);
|
||||
}
|
||||
|
||||
private sealed class IntConstantEditor : IConstantEditor
|
||||
{
|
||||
private readonly int? _minimum;
|
||||
private readonly int? _maximum;
|
||||
private readonly float _speed;
|
||||
private readonly float _relativeSpeed;
|
||||
private readonly float _factor;
|
||||
private readonly float _bias;
|
||||
private readonly string _format;
|
||||
|
||||
public IntConstantEditor(int? minimum, int? maximum, float speed, float relativeSpeed, float factor, float bias, string unit)
|
||||
{
|
||||
_minimum = minimum;
|
||||
_maximum = maximum;
|
||||
_speed = speed;
|
||||
_relativeSpeed = relativeSpeed;
|
||||
_factor = factor;
|
||||
_bias = bias;
|
||||
_format = "%d";
|
||||
if (unit.Length > 0)
|
||||
_format = $"{_format} {unit.Replace("%", "%%")}";
|
||||
}
|
||||
|
||||
public bool Draw(Span<float> values, bool disabled)
|
||||
{
|
||||
var spacing = ImGui.GetStyle().ItemInnerSpacing.X;
|
||||
var fieldWidth = (ImGui.CalcItemWidth() - (values.Length - 1) * spacing) / values.Length;
|
||||
|
||||
var ret = false;
|
||||
|
||||
// Not using DragScalarN because of _relativeSpeed and other points of lost flexibility.
|
||||
for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx)
|
||||
{
|
||||
if (valueIdx > 0)
|
||||
ImGui.SameLine(0.0f, spacing);
|
||||
|
||||
ImGui.SetNextItemWidth(MathF.Round(fieldWidth * (valueIdx + 1)) - MathF.Round(fieldWidth * valueIdx));
|
||||
|
||||
var value = (int)Math.Clamp(MathF.Round((values[valueIdx] - _bias) / _factor), int.MinValue, int.MaxValue);
|
||||
if (disabled)
|
||||
{
|
||||
ImGui.DragInt($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), value, value, _format);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ImGui.DragInt($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), _minimum ?? 0, _maximum ?? 0,
|
||||
_format))
|
||||
{
|
||||
values[valueIdx] = Clamp(value) * _factor + _bias;
|
||||
ret = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private int Clamp(int value)
|
||||
=> Math.Clamp(value, _minimum ?? int.MinValue, _maximum ?? int.MaxValue);
|
||||
}
|
||||
|
||||
private sealed class ColorConstantEditor : IConstantEditor
|
||||
{
|
||||
private readonly bool _squaredRgb;
|
||||
private readonly bool _clamped;
|
||||
|
||||
public ColorConstantEditor(bool squaredRgb, bool clamped)
|
||||
{
|
||||
_squaredRgb = squaredRgb;
|
||||
_clamped = clamped;
|
||||
}
|
||||
|
||||
public bool Draw(Span<float> values, bool disabled)
|
||||
{
|
||||
switch (values.Length)
|
||||
{
|
||||
case 3:
|
||||
{
|
||||
var value = new Vector3(values);
|
||||
if (_squaredRgb)
|
||||
value = PseudoSqrtRgb(value);
|
||||
if (!ImGui.ColorEdit3("##0", ref value, ImGuiColorEditFlags.Float | (_clamped ? 0 : ImGuiColorEditFlags.HDR)) || disabled)
|
||||
return false;
|
||||
|
||||
if (_squaredRgb)
|
||||
value = PseudoSquareRgb(value);
|
||||
if (_clamped)
|
||||
value = Vector3.Clamp(value, Vector3.Zero, Vector3.One);
|
||||
value.CopyTo(values);
|
||||
return true;
|
||||
}
|
||||
case 4:
|
||||
{
|
||||
var value = new Vector4(values);
|
||||
if (_squaredRgb)
|
||||
value = PseudoSqrtRgb(value);
|
||||
if (!ImGui.ColorEdit4("##0", ref value,
|
||||
ImGuiColorEditFlags.Float | ImGuiColorEditFlags.AlphaPreviewHalf | (_clamped ? 0 : ImGuiColorEditFlags.HDR))
|
||||
|| disabled)
|
||||
return false;
|
||||
|
||||
if (_squaredRgb)
|
||||
value = PseudoSquareRgb(value);
|
||||
if (_clamped)
|
||||
value = Vector4.Clamp(value, Vector4.Zero, Vector4.One);
|
||||
value.CopyTo(values);
|
||||
return true;
|
||||
}
|
||||
default: return FloatConstantEditor.Default.Draw(values, disabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class EnumConstantEditor : IConstantEditor
|
||||
{
|
||||
private readonly IReadOnlyList<(string Label, float Value, string Description)> _values;
|
||||
|
||||
public EnumConstantEditor(IReadOnlyList<(string Label, float Value, string Description)> values)
|
||||
=> _values = values;
|
||||
|
||||
public bool Draw(Span<float> values, bool disabled)
|
||||
{
|
||||
var spacing = ImGui.GetStyle().ItemInnerSpacing.X;
|
||||
var fieldWidth = (ImGui.CalcItemWidth() - (values.Length - 1) * spacing) / values.Length;
|
||||
|
||||
var ret = false;
|
||||
|
||||
for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx)
|
||||
{
|
||||
using var id = ImRaii.PushId(valueIdx);
|
||||
if (valueIdx > 0)
|
||||
ImGui.SameLine(0.0f, spacing);
|
||||
|
||||
ImGui.SetNextItemWidth(MathF.Round(fieldWidth * (valueIdx + 1)) - MathF.Round(fieldWidth * valueIdx));
|
||||
|
||||
var currentValue = values[valueIdx];
|
||||
var currentLabel = _values.FirstOrNull(v => v.Value == currentValue)?.Label
|
||||
?? currentValue.ToString(CultureInfo.CurrentCulture);
|
||||
ret = disabled
|
||||
? ImGui.InputText(string.Empty, ref currentLabel, (uint)currentLabel.Length, ImGuiInputTextFlags.ReadOnly)
|
||||
: DrawCombo(currentLabel, ref values[valueIdx]);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private bool DrawCombo(string label, ref float currentValue)
|
||||
{
|
||||
using var c = ImRaii.Combo(string.Empty, label);
|
||||
if (!c)
|
||||
return false;
|
||||
|
||||
var ret = false;
|
||||
foreach (var (valueLabel, value, valueDescription) in _values)
|
||||
{
|
||||
if (ImGui.Selectable(valueLabel, value == currentValue))
|
||||
{
|
||||
currentValue = value;
|
||||
ret = true;
|
||||
}
|
||||
|
||||
if (valueDescription.Length > 0)
|
||||
ImGuiUtil.SelectableHelpMarker(valueDescription);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,783 +0,0 @@
|
|||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using ImGuiNET;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Raii;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.GameData.Files.MaterialStructs;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Interop.Hooks.Objects;
|
||||
using Penumbra.Interop.MaterialPreview;
|
||||
using Penumbra.String;
|
||||
using Penumbra.String.Classes;
|
||||
using Penumbra.UI.Classes;
|
||||
using static Penumbra.GameData.Files.ShpkFile;
|
||||
|
||||
namespace Penumbra.UI.AdvancedWindow;
|
||||
|
||||
public partial class ModEditWindow
|
||||
{
|
||||
private sealed class MtrlTab : IWritable, IDisposable
|
||||
{
|
||||
private const int ShpkPrefixLength = 16;
|
||||
|
||||
private static readonly CiByteString ShpkPrefix = CiByteString.FromSpanUnsafe("shader/sm5/shpk/"u8, true, true, true);
|
||||
|
||||
private readonly ModEditWindow _edit;
|
||||
public readonly MtrlFile Mtrl;
|
||||
public readonly string FilePath;
|
||||
public readonly bool Writable;
|
||||
|
||||
private string[]? _shpkNames;
|
||||
|
||||
public string ShaderHeader = "Shader###Shader";
|
||||
public FullPath LoadedShpkPath = FullPath.Empty;
|
||||
public string LoadedShpkPathName = string.Empty;
|
||||
public string LoadedShpkDevkitPathName = string.Empty;
|
||||
public string ShaderComment = string.Empty;
|
||||
public ShpkFile? AssociatedShpk;
|
||||
public JObject? AssociatedShpkDevkit;
|
||||
|
||||
public readonly string LoadedBaseDevkitPathName;
|
||||
public readonly JObject? AssociatedBaseDevkit;
|
||||
|
||||
// Shader Key State
|
||||
public readonly
|
||||
List<(string Label, int Index, string Description, bool MonoFont, IReadOnlyList<(string Label, uint Value, string Description)>
|
||||
Values)> ShaderKeys = new(16);
|
||||
|
||||
public readonly HashSet<int> VertexShaders = new(16);
|
||||
public readonly HashSet<int> PixelShaders = new(16);
|
||||
public bool ShadersKnown;
|
||||
public string VertexShadersString = "Vertex Shaders: ???";
|
||||
public string PixelShadersString = "Pixel Shaders: ???";
|
||||
|
||||
// Textures & Samplers
|
||||
public readonly List<(string Label, int TextureIndex, int SamplerIndex, string Description, bool MonoFont)> Textures = new(4);
|
||||
|
||||
public readonly HashSet<int> UnfoldedTextures = new(4);
|
||||
public readonly HashSet<uint> SamplerIds = new(16);
|
||||
public float TextureLabelWidth;
|
||||
|
||||
// Material Constants
|
||||
public readonly
|
||||
List<(string Header, List<(string Label, int ConstantIndex, Range Slice, string Description, bool MonoFont, IConstantEditor Editor)>
|
||||
Constants)> Constants = new(16);
|
||||
|
||||
// Live-Previewers
|
||||
public readonly List<LiveMaterialPreviewer> MaterialPreviewers = new(4);
|
||||
public readonly List<LiveColorTablePreviewer> ColorTablePreviewers = new(4);
|
||||
public int HighlightedColorTableRow = -1;
|
||||
public readonly Stopwatch HighlightTime = new();
|
||||
|
||||
public FullPath FindAssociatedShpk(out string defaultPath, out Utf8GamePath defaultGamePath)
|
||||
{
|
||||
defaultPath = GamePaths.Shader.ShpkPath(Mtrl.ShaderPackage.Name);
|
||||
if (!Utf8GamePath.FromString(defaultPath, out defaultGamePath))
|
||||
return FullPath.Empty;
|
||||
|
||||
return _edit.FindBestMatch(defaultGamePath);
|
||||
}
|
||||
|
||||
public string[] GetShpkNames()
|
||||
{
|
||||
if (null != _shpkNames)
|
||||
return _shpkNames;
|
||||
|
||||
var names = new HashSet<string>(StandardShaderPackages);
|
||||
names.UnionWith(_edit.FindPathsStartingWith(ShpkPrefix).Select(path => path.ToString()[ShpkPrefixLength..]));
|
||||
|
||||
_shpkNames = names.ToArray();
|
||||
Array.Sort(_shpkNames);
|
||||
|
||||
return _shpkNames;
|
||||
}
|
||||
|
||||
public void LoadShpk(FullPath path)
|
||||
{
|
||||
ShaderHeader = $"Shader ({Mtrl.ShaderPackage.Name})###Shader";
|
||||
|
||||
try
|
||||
{
|
||||
LoadedShpkPath = path;
|
||||
var data = LoadedShpkPath.IsRooted
|
||||
? File.ReadAllBytes(LoadedShpkPath.FullName)
|
||||
: _edit._gameData.GetFile(LoadedShpkPath.InternalName.ToString())?.Data;
|
||||
AssociatedShpk = data?.Length > 0 ? new ShpkFile(data) : throw new Exception("Failure to load file data.");
|
||||
LoadedShpkPathName = path.ToPath();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LoadedShpkPath = FullPath.Empty;
|
||||
LoadedShpkPathName = string.Empty;
|
||||
AssociatedShpk = null;
|
||||
Penumbra.Messager.NotificationMessage(e, $"Could not load {LoadedShpkPath.ToPath()}.", NotificationType.Error, false);
|
||||
}
|
||||
|
||||
if (LoadedShpkPath.InternalName.IsEmpty)
|
||||
{
|
||||
AssociatedShpkDevkit = null;
|
||||
LoadedShpkDevkitPathName = string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
AssociatedShpkDevkit =
|
||||
TryLoadShpkDevkit(Path.GetFileNameWithoutExtension(Mtrl.ShaderPackage.Name), out LoadedShpkDevkitPathName);
|
||||
}
|
||||
|
||||
UpdateShaderKeys();
|
||||
Update();
|
||||
}
|
||||
|
||||
private JObject? TryLoadShpkDevkit(string shpkBaseName, out string devkitPathName)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Utf8GamePath.FromString("penumbra/shpk_devkit/" + shpkBaseName + ".json", out var devkitPath))
|
||||
throw new Exception("Could not assemble ShPk dev-kit path.");
|
||||
|
||||
var devkitFullPath = _edit.FindBestMatch(devkitPath);
|
||||
if (!devkitFullPath.IsRooted)
|
||||
throw new Exception("Could not resolve ShPk dev-kit path.");
|
||||
|
||||
devkitPathName = devkitFullPath.FullName;
|
||||
return JObject.Parse(File.ReadAllText(devkitFullPath.FullName));
|
||||
}
|
||||
catch
|
||||
{
|
||||
devkitPathName = string.Empty;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private T? TryGetShpkDevkitData<T>(string category, uint? id, bool mayVary) where T : class
|
||||
=> TryGetShpkDevkitData<T>(AssociatedShpkDevkit, LoadedShpkDevkitPathName, category, id, mayVary)
|
||||
?? TryGetShpkDevkitData<T>(AssociatedBaseDevkit, LoadedBaseDevkitPathName, category, id, mayVary);
|
||||
|
||||
private T? TryGetShpkDevkitData<T>(JObject? devkit, string devkitPathName, string category, uint? id, bool mayVary) where T : class
|
||||
{
|
||||
if (devkit == null)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
var data = devkit[category];
|
||||
if (id.HasValue)
|
||||
data = data?[id.Value.ToString()];
|
||||
|
||||
if (mayVary && (data as JObject)?["Vary"] != null)
|
||||
{
|
||||
var selector = BuildSelector(data!["Vary"]!
|
||||
.Select(key => (uint)key)
|
||||
.Select(key => Mtrl.GetShaderKey(key)?.Value ?? AssociatedShpk!.GetMaterialKeyById(key)!.Value.DefaultValue));
|
||||
var index = (int)data["Selectors"]![selector.ToString()]!;
|
||||
data = data["Items"]![index];
|
||||
}
|
||||
|
||||
return data?.ToObject(typeof(T)) as T;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Some element in the JSON was undefined or invalid (wrong type, key that doesn't exist in the ShPk, index out of range, …)
|
||||
Penumbra.Log.Error($"Error while traversing the ShPk dev-kit file at {devkitPathName}: {e}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateShaderKeys()
|
||||
{
|
||||
ShaderKeys.Clear();
|
||||
if (AssociatedShpk != null)
|
||||
foreach (var key in AssociatedShpk.MaterialKeys)
|
||||
{
|
||||
var dkData = TryGetShpkDevkitData<DevkitShaderKey>("ShaderKeys", key.Id, false);
|
||||
var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label);
|
||||
|
||||
var valueSet = new HashSet<uint>(key.Values);
|
||||
if (dkData != null)
|
||||
valueSet.UnionWith(dkData.Values.Keys);
|
||||
|
||||
var mtrlKeyIndex = Mtrl.FindOrAddShaderKey(key.Id, key.DefaultValue);
|
||||
var values = valueSet.Select<uint, (string Label, uint Value, string Description)>(value =>
|
||||
{
|
||||
if (dkData != null && dkData.Values.TryGetValue(value, out var dkValue))
|
||||
return (dkValue.Label.Length > 0 ? dkValue.Label : $"0x{value:X8}", value, dkValue.Description);
|
||||
|
||||
return ($"0x{value:X8}", value, string.Empty);
|
||||
}).ToArray();
|
||||
Array.Sort(values, (x, y) =>
|
||||
{
|
||||
if (x.Value == key.DefaultValue)
|
||||
return -1;
|
||||
if (y.Value == key.DefaultValue)
|
||||
return 1;
|
||||
|
||||
return string.Compare(x.Label, y.Label, StringComparison.Ordinal);
|
||||
});
|
||||
ShaderKeys.Add((hasDkLabel ? dkData!.Label : $"0x{key.Id:X8}", mtrlKeyIndex, dkData?.Description ?? string.Empty,
|
||||
!hasDkLabel, values));
|
||||
}
|
||||
else
|
||||
foreach (var (key, index) in Mtrl.ShaderPackage.ShaderKeys.WithIndex())
|
||||
ShaderKeys.Add(($"0x{key.Category:X8}", index, string.Empty, true, Array.Empty<(string, uint, string)>()));
|
||||
}
|
||||
|
||||
private void UpdateShaders()
|
||||
{
|
||||
VertexShaders.Clear();
|
||||
PixelShaders.Clear();
|
||||
if (AssociatedShpk == null)
|
||||
{
|
||||
ShadersKnown = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
ShadersKnown = true;
|
||||
var systemKeySelectors = AllSelectors(AssociatedShpk.SystemKeys).ToArray();
|
||||
var sceneKeySelectors = AllSelectors(AssociatedShpk.SceneKeys).ToArray();
|
||||
var subViewKeySelectors = AllSelectors(AssociatedShpk.SubViewKeys).ToArray();
|
||||
var materialKeySelector =
|
||||
BuildSelector(AssociatedShpk.MaterialKeys.Select(key => Mtrl.GetOrAddShaderKey(key.Id, key.DefaultValue).Value));
|
||||
foreach (var systemKeySelector in systemKeySelectors)
|
||||
{
|
||||
foreach (var sceneKeySelector in sceneKeySelectors)
|
||||
{
|
||||
foreach (var subViewKeySelector in subViewKeySelectors)
|
||||
{
|
||||
var selector = BuildSelector(systemKeySelector, sceneKeySelector, materialKeySelector, subViewKeySelector);
|
||||
var node = AssociatedShpk.GetNodeBySelector(selector);
|
||||
if (node.HasValue)
|
||||
foreach (var pass in node.Value.Passes)
|
||||
{
|
||||
VertexShaders.Add((int)pass.VertexShader);
|
||||
PixelShaders.Add((int)pass.PixelShader);
|
||||
}
|
||||
else
|
||||
ShadersKnown = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var vertexShaders = VertexShaders.OrderBy(i => i).Select(i => $"#{i}");
|
||||
var pixelShaders = PixelShaders.OrderBy(i => i).Select(i => $"#{i}");
|
||||
|
||||
VertexShadersString = $"Vertex Shaders: {string.Join(", ", ShadersKnown ? vertexShaders : vertexShaders.Append("???"))}";
|
||||
PixelShadersString = $"Pixel Shaders: {string.Join(", ", ShadersKnown ? pixelShaders : pixelShaders.Append("???"))}";
|
||||
|
||||
ShaderComment = TryGetShpkDevkitData<string>("Comment", null, true) ?? string.Empty;
|
||||
}
|
||||
|
||||
private void UpdateTextures()
|
||||
{
|
||||
Textures.Clear();
|
||||
SamplerIds.Clear();
|
||||
if (AssociatedShpk == null)
|
||||
{
|
||||
SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId));
|
||||
if (Mtrl.HasTable)
|
||||
SamplerIds.Add(TableSamplerId);
|
||||
|
||||
foreach (var (sampler, index) in Mtrl.ShaderPackage.Samplers.WithIndex())
|
||||
Textures.Add(($"0x{sampler.SamplerId:X8}", sampler.TextureIndex, index, string.Empty, true));
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var index in VertexShaders)
|
||||
SamplerIds.UnionWith(AssociatedShpk.VertexShaders[index].Samplers.Select(sampler => sampler.Id));
|
||||
foreach (var index in PixelShaders)
|
||||
SamplerIds.UnionWith(AssociatedShpk.PixelShaders[index].Samplers.Select(sampler => sampler.Id));
|
||||
if (!ShadersKnown)
|
||||
{
|
||||
SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId));
|
||||
if (Mtrl.HasTable)
|
||||
SamplerIds.Add(TableSamplerId);
|
||||
}
|
||||
|
||||
foreach (var samplerId in SamplerIds)
|
||||
{
|
||||
var shpkSampler = AssociatedShpk.GetSamplerById(samplerId);
|
||||
if (shpkSampler is not { Slot: 2 })
|
||||
continue;
|
||||
|
||||
var dkData = TryGetShpkDevkitData<DevkitSampler>("Samplers", samplerId, true);
|
||||
var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label);
|
||||
|
||||
var sampler = Mtrl.GetOrAddSampler(samplerId, dkData?.DefaultTexture ?? string.Empty, out var samplerIndex);
|
||||
Textures.Add((hasDkLabel ? dkData!.Label : shpkSampler.Value.Name, sampler.TextureIndex, samplerIndex,
|
||||
dkData?.Description ?? string.Empty, !hasDkLabel));
|
||||
}
|
||||
|
||||
if (SamplerIds.Contains(TableSamplerId))
|
||||
Mtrl.HasTable = true;
|
||||
}
|
||||
|
||||
Textures.Sort((x, y) => string.CompareOrdinal(x.Label, y.Label));
|
||||
|
||||
TextureLabelWidth = 50f * UiHelpers.Scale;
|
||||
|
||||
float helpWidth;
|
||||
using (var _ = ImRaii.PushFont(UiBuilder.IconFont))
|
||||
{
|
||||
helpWidth = ImGui.GetStyle().ItemSpacing.X + ImGui.CalcTextSize(FontAwesomeIcon.InfoCircle.ToIconString()).X;
|
||||
}
|
||||
|
||||
foreach (var (label, _, _, description, monoFont) in Textures)
|
||||
{
|
||||
if (!monoFont)
|
||||
TextureLabelWidth = Math.Max(TextureLabelWidth, ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f));
|
||||
}
|
||||
|
||||
using (var _ = ImRaii.PushFont(UiBuilder.MonoFont))
|
||||
{
|
||||
foreach (var (label, _, _, description, monoFont) in Textures)
|
||||
{
|
||||
if (monoFont)
|
||||
TextureLabelWidth = Math.Max(TextureLabelWidth,
|
||||
ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f));
|
||||
}
|
||||
}
|
||||
|
||||
TextureLabelWidth = TextureLabelWidth / UiHelpers.Scale + 4;
|
||||
}
|
||||
|
||||
private void UpdateConstants()
|
||||
{
|
||||
static List<T> FindOrAddGroup<T>(List<(string, List<T>)> groups, string name)
|
||||
{
|
||||
foreach (var (groupName, group) in groups)
|
||||
{
|
||||
if (string.Equals(name, groupName, StringComparison.Ordinal))
|
||||
return group;
|
||||
}
|
||||
|
||||
var newGroup = new List<T>(16);
|
||||
groups.Add((name, newGroup));
|
||||
return newGroup;
|
||||
}
|
||||
|
||||
Constants.Clear();
|
||||
if (AssociatedShpk == null)
|
||||
{
|
||||
var fcGroup = FindOrAddGroup(Constants, "Further Constants");
|
||||
foreach (var (constant, index) in Mtrl.ShaderPackage.Constants.WithIndex())
|
||||
{
|
||||
var values = Mtrl.GetConstantValues(constant);
|
||||
for (var i = 0; i < values.Length; i += 4)
|
||||
{
|
||||
fcGroup.Add(($"0x{constant.Id:X8}", index, i..Math.Min(i + 4, values.Length), string.Empty, true,
|
||||
FloatConstantEditor.Default));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var prefix = AssociatedShpk.GetConstantById(MaterialParamsConstantId)?.Name ?? string.Empty;
|
||||
foreach (var shpkConstant in AssociatedShpk.MaterialParams)
|
||||
{
|
||||
if ((shpkConstant.ByteSize & 0x3) != 0)
|
||||
continue;
|
||||
|
||||
var constant = Mtrl.GetOrAddConstant(shpkConstant.Id, shpkConstant.ByteSize >> 2, out var constantIndex);
|
||||
var values = Mtrl.GetConstantValues(constant);
|
||||
var handledElements = new IndexSet(values.Length, false);
|
||||
|
||||
var dkData = TryGetShpkDevkitData<DevkitConstant[]>("Constants", shpkConstant.Id, true);
|
||||
if (dkData != null)
|
||||
foreach (var dkConstant in dkData)
|
||||
{
|
||||
var offset = (int)dkConstant.Offset;
|
||||
var length = values.Length - offset;
|
||||
if (dkConstant.Length.HasValue)
|
||||
length = Math.Min(length, (int)dkConstant.Length.Value);
|
||||
if (length <= 0)
|
||||
continue;
|
||||
|
||||
var editor = dkConstant.CreateEditor();
|
||||
if (editor != null)
|
||||
FindOrAddGroup(Constants, dkConstant.Group.Length > 0 ? dkConstant.Group : "Further Constants")
|
||||
.Add((dkConstant.Label, constantIndex, offset..(offset + length), dkConstant.Description, false, editor));
|
||||
handledElements.AddRange(offset, length);
|
||||
}
|
||||
|
||||
var fcGroup = FindOrAddGroup(Constants, "Further Constants");
|
||||
foreach (var (start, end) in handledElements.Ranges(complement:true))
|
||||
{
|
||||
if ((shpkConstant.ByteOffset & 0x3) == 0)
|
||||
{
|
||||
var offset = shpkConstant.ByteOffset >> 2;
|
||||
for (int i = (start & ~0x3) - (offset & 0x3), j = offset >> 2; i < end; i += 4, ++j)
|
||||
{
|
||||
var rangeStart = Math.Max(i, start);
|
||||
var rangeEnd = Math.Min(i + 4, end);
|
||||
if (rangeEnd > rangeStart)
|
||||
fcGroup.Add((
|
||||
$"{prefix}[{j:D2}]{VectorSwizzle((offset + rangeStart) & 0x3, (offset + rangeEnd - 1) & 0x3)} (0x{shpkConstant.Id:X8})",
|
||||
constantIndex, rangeStart..rangeEnd, string.Empty, true, FloatConstantEditor.Default));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var i = start; i < end; i += 4)
|
||||
{
|
||||
fcGroup.Add(($"0x{shpkConstant.Id:X8}", constantIndex, i..Math.Min(i + 4, end), string.Empty, true,
|
||||
FloatConstantEditor.Default));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Constants.RemoveAll(group => group.Constants.Count == 0);
|
||||
Constants.Sort((x, y) =>
|
||||
{
|
||||
if (string.Equals(x.Header, "Further Constants", StringComparison.Ordinal))
|
||||
return 1;
|
||||
if (string.Equals(y.Header, "Further Constants", StringComparison.Ordinal))
|
||||
return -1;
|
||||
|
||||
return string.Compare(x.Header, y.Header, StringComparison.Ordinal);
|
||||
});
|
||||
// HACK the Replace makes w appear after xyz, for the cbuffer-location-based naming scheme
|
||||
foreach (var (_, group) in Constants)
|
||||
{
|
||||
group.Sort((x, y) => string.CompareOrdinal(
|
||||
x.MonoFont ? x.Label.Replace("].w", "].{") : x.Label,
|
||||
y.MonoFont ? y.Label.Replace("].w", "].{") : y.Label));
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe void BindToMaterialInstances()
|
||||
{
|
||||
UnbindFromMaterialInstances();
|
||||
|
||||
var instances = MaterialInfo.FindMaterials(_edit._resourceTreeFactory.GetLocalPlayerRelatedCharacters().Select(ch => ch.Address),
|
||||
FilePath);
|
||||
|
||||
var foundMaterials = new HashSet<nint>();
|
||||
foreach (var materialInfo in instances)
|
||||
{
|
||||
var material = materialInfo.GetDrawObjectMaterial(_edit._objects);
|
||||
if (foundMaterials.Contains((nint)material))
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
MaterialPreviewers.Add(new LiveMaterialPreviewer(_edit._objects, materialInfo));
|
||||
foundMaterials.Add((nint)material);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// Carry on without that previewer.
|
||||
}
|
||||
}
|
||||
|
||||
UpdateMaterialPreview();
|
||||
|
||||
if (!Mtrl.HasTable)
|
||||
return;
|
||||
|
||||
foreach (var materialInfo in instances)
|
||||
{
|
||||
try
|
||||
{
|
||||
ColorTablePreviewers.Add(new LiveColorTablePreviewer(_edit._objects, _edit._framework, materialInfo));
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// Carry on without that previewer.
|
||||
}
|
||||
}
|
||||
|
||||
UpdateColorTablePreview();
|
||||
}
|
||||
|
||||
private void UnbindFromMaterialInstances()
|
||||
{
|
||||
foreach (var previewer in MaterialPreviewers)
|
||||
previewer.Dispose();
|
||||
MaterialPreviewers.Clear();
|
||||
|
||||
foreach (var previewer in ColorTablePreviewers)
|
||||
previewer.Dispose();
|
||||
ColorTablePreviewers.Clear();
|
||||
}
|
||||
|
||||
private unsafe void UnbindFromDrawObjectMaterialInstances(CharacterBase* characterBase)
|
||||
{
|
||||
for (var i = MaterialPreviewers.Count; i-- > 0;)
|
||||
{
|
||||
var previewer = MaterialPreviewers[i];
|
||||
if (previewer.DrawObject != characterBase)
|
||||
continue;
|
||||
|
||||
previewer.Dispose();
|
||||
MaterialPreviewers.RemoveAt(i);
|
||||
}
|
||||
|
||||
for (var i = ColorTablePreviewers.Count; i-- > 0;)
|
||||
{
|
||||
var previewer = ColorTablePreviewers[i];
|
||||
if (previewer.DrawObject != characterBase)
|
||||
continue;
|
||||
|
||||
previewer.Dispose();
|
||||
ColorTablePreviewers.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetShaderPackageFlags(uint shPkFlags)
|
||||
{
|
||||
foreach (var previewer in MaterialPreviewers)
|
||||
previewer.SetShaderPackageFlags(shPkFlags);
|
||||
}
|
||||
|
||||
public void SetMaterialParameter(uint parameterCrc, Index offset, Span<float> value)
|
||||
{
|
||||
foreach (var previewer in MaterialPreviewers)
|
||||
previewer.SetMaterialParameter(parameterCrc, offset, value);
|
||||
}
|
||||
|
||||
public void SetSamplerFlags(uint samplerCrc, uint samplerFlags)
|
||||
{
|
||||
foreach (var previewer in MaterialPreviewers)
|
||||
previewer.SetSamplerFlags(samplerCrc, samplerFlags);
|
||||
}
|
||||
|
||||
private void UpdateMaterialPreview()
|
||||
{
|
||||
SetShaderPackageFlags(Mtrl.ShaderPackage.Flags);
|
||||
foreach (var constant in Mtrl.ShaderPackage.Constants)
|
||||
{
|
||||
var values = Mtrl.GetConstantValues(constant);
|
||||
if (values != null)
|
||||
SetMaterialParameter(constant.Id, 0, values);
|
||||
}
|
||||
|
||||
foreach (var sampler in Mtrl.ShaderPackage.Samplers)
|
||||
SetSamplerFlags(sampler.SamplerId, sampler.Flags);
|
||||
}
|
||||
|
||||
public void HighlightColorTableRow(int rowIdx)
|
||||
{
|
||||
var oldRowIdx = HighlightedColorTableRow;
|
||||
|
||||
if (HighlightedColorTableRow != rowIdx)
|
||||
{
|
||||
HighlightedColorTableRow = rowIdx;
|
||||
HighlightTime.Restart();
|
||||
}
|
||||
|
||||
if (oldRowIdx >= 0)
|
||||
UpdateColorTableRowPreview(oldRowIdx);
|
||||
if (rowIdx >= 0)
|
||||
UpdateColorTableRowPreview(rowIdx);
|
||||
}
|
||||
|
||||
public void CancelColorTableHighlight()
|
||||
{
|
||||
var rowIdx = HighlightedColorTableRow;
|
||||
|
||||
HighlightedColorTableRow = -1;
|
||||
HighlightTime.Reset();
|
||||
|
||||
if (rowIdx >= 0)
|
||||
UpdateColorTableRowPreview(rowIdx);
|
||||
}
|
||||
|
||||
public void UpdateColorTableRowPreview(int rowIdx)
|
||||
{
|
||||
if (ColorTablePreviewers.Count == 0)
|
||||
return;
|
||||
|
||||
if (!Mtrl.HasTable)
|
||||
return;
|
||||
|
||||
var row = new 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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, ");
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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." ),
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue