Merge branch 'feature/material-editor-2099'

This commit is contained in:
Ottermandias 2023-08-31 18:32:43 +02:00
commit 82cecdaf7d
24 changed files with 2750 additions and 1321 deletions

@ -1 +1 @@
Subproject commit c8394607addd29cb7f8ae3257f635a4486c40a63
Subproject commit 728dd8c33f8b43f7a2725ac7c8886fe7cb3f04a9

@ -1 +1 @@
Subproject commit 97643cad67b6981c3ee510d1ca12c4321e6a80bf
Subproject commit 1c68fd5efb23798d13154c1de0ad010db319abe2

View file

@ -0,0 +1,131 @@
using System;
using System.Threading;
using Dalamud.Game;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
using Penumbra.GameData.Files;
namespace Penumbra.Interop.MaterialPreview;
public sealed unsafe class LiveColorSetPreviewer : LiveMaterialPreviewerBase
{
public const int TextureWidth = 4;
public const int TextureHeight = MtrlFile.ColorSet.RowArray.NumRows;
public const int TextureLength = TextureWidth * TextureHeight * 4;
private readonly Framework _framework;
private readonly Texture** _colorSetTexture;
private readonly Texture* _originalColorSetTexture;
private Half[] _colorSet;
private bool _updatePending;
public Half[] ColorSet
=> _colorSet;
public LiveColorSetPreviewer(IObjectTable objects, Framework framework, MaterialInfo materialInfo)
: base(objects, materialInfo)
{
_framework = framework;
var mtrlHandle = Material->MaterialResourceHandle;
if (mtrlHandle == null)
throw new InvalidOperationException("Material doesn't have a resource handle");
var colorSetTextures = ((Structs.CharacterBaseExt*)DrawObject)->ColorSetTextures;
if (colorSetTextures == null)
throw new InvalidOperationException("Draw object doesn't have color set textures");
_colorSetTexture = colorSetTextures + (MaterialInfo.ModelSlot * 4 + MaterialInfo.MaterialSlot);
_originalColorSetTexture = *_colorSetTexture;
if (_originalColorSetTexture == null)
throw new InvalidOperationException("Material doesn't have a color set");
Structs.TextureUtility.IncRef(_originalColorSetTexture);
_colorSet = new Half[TextureLength];
_updatePending = true;
framework.Update += OnFrameworkUpdate;
}
protected override void Clear(bool disposing, bool reset)
{
_framework.Update -= OnFrameworkUpdate;
base.Clear(disposing, reset);
if (reset)
{
var oldTexture = (Texture*)Interlocked.Exchange(ref *(nint*)_colorSetTexture, (nint)_originalColorSetTexture);
if (oldTexture != null)
Structs.TextureUtility.DecRef(oldTexture);
}
else
{
Structs.TextureUtility.DecRef(_originalColorSetTexture);
}
}
public void ScheduleUpdate()
{
_updatePending = true;
}
private void OnFrameworkUpdate(Framework _)
{
if (!_updatePending)
return;
_updatePending = false;
if (!CheckValidity())
return;
var textureSize = stackalloc int[2];
textureSize[0] = TextureWidth;
textureSize[1] = TextureHeight;
var newTexture = Structs.TextureUtility.Create2D(Device.Instance(), textureSize, 1, 0x2460, 0x80000804, 7);
if (newTexture == null)
return;
bool success;
lock (_colorSet)
{
fixed (Half* colorSet = _colorSet)
{
success = Structs.TextureUtility.InitializeContents(newTexture, colorSet);
}
}
if (success)
{
var oldTexture = (Texture*)Interlocked.Exchange(ref *(nint*)_colorSetTexture, (nint)newTexture);
if (oldTexture != null)
Structs.TextureUtility.DecRef(oldTexture);
}
else
{
Structs.TextureUtility.DecRef(newTexture);
}
}
protected override bool IsStillValid()
{
if (!base.IsStillValid())
return false;
var colorSetTextures = ((Structs.CharacterBaseExt*)DrawObject)->ColorSetTextures;
if (colorSetTextures == null)
return false;
if (_colorSetTexture != colorSetTextures + (MaterialInfo.ModelSlot * 4 + MaterialInfo.MaterialSlot))
return false;
return true;
}
}

View file

@ -0,0 +1,149 @@
using System;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
namespace Penumbra.Interop.MaterialPreview;
public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase
{
private readonly ShaderPackage* _shaderPackage;
private readonly uint _originalShPkFlags;
private readonly float[] _originalMaterialParameter;
private readonly uint[] _originalSamplerFlags;
public LiveMaterialPreviewer(IObjectTable objects, MaterialInfo materialInfo)
: base(objects, materialInfo)
{
var mtrlHandle = Material->MaterialResourceHandle;
if (mtrlHandle == null)
throw new InvalidOperationException("Material doesn't have a resource handle");
var shpkHandle = ((Structs.MtrlResource*)mtrlHandle)->ShpkResourceHandle;
if (shpkHandle == null)
throw new InvalidOperationException("Material doesn't have a ShPk resource handle");
_shaderPackage = shpkHandle->ShaderPackage;
if (_shaderPackage == null)
throw new InvalidOperationException("Material doesn't have a shader package");
var material = (Structs.Material*)Material;
_originalShPkFlags = material->ShaderPackageFlags;
if (material->MaterialParameter->TryGetBuffer(out var materialParameter))
_originalMaterialParameter = materialParameter.ToArray();
else
_originalMaterialParameter = Array.Empty<float>();
_originalSamplerFlags = new uint[material->TextureCount];
for (var i = 0; i < _originalSamplerFlags.Length; ++i)
_originalSamplerFlags[i] = material->Textures[i].SamplerFlags;
}
protected override void Clear(bool disposing, bool reset)
{
base.Clear(disposing, reset);
if (reset)
{
var material = (Structs.Material*)Material;
material->ShaderPackageFlags = _originalShPkFlags;
if (material->MaterialParameter->TryGetBuffer(out var materialParameter))
_originalMaterialParameter.AsSpan().CopyTo(materialParameter);
for (var i = 0; i < _originalSamplerFlags.Length; ++i)
material->Textures[i].SamplerFlags = _originalSamplerFlags[i];
}
}
public void SetShaderPackageFlags(uint shPkFlags)
{
if (!CheckValidity())
return;
((Structs.Material*)Material)->ShaderPackageFlags = shPkFlags;
}
public void SetMaterialParameter(uint parameterCrc, Index offset, Span<float> value)
{
if (!CheckValidity())
return;
var constantBuffer = ((Structs.Material*)Material)->MaterialParameter;
if (constantBuffer == null)
return;
if (!constantBuffer->TryGetBuffer(out var buffer))
return;
for (var i = 0; i < _shaderPackage->MaterialElementCount; ++i)
{
ref var parameter = ref _shaderPackage->MaterialElements[i];
if (parameter.CRC == parameterCrc)
{
if ((parameter.Offset & 0x3) != 0
|| (parameter.Size & 0x3) != 0
|| (parameter.Offset + parameter.Size) >> 2 > buffer.Length)
return;
value.TryCopyTo(buffer.Slice(parameter.Offset >> 2, parameter.Size >> 2)[offset..]);
return;
}
}
}
public void SetSamplerFlags(uint samplerCrc, uint samplerFlags)
{
if (!CheckValidity())
return;
var id = 0u;
var found = false;
var samplers = (Structs.ShaderPackageUtility.Sampler*)_shaderPackage->Samplers;
for (var i = 0; i < _shaderPackage->SamplerCount; ++i)
{
if (samplers[i].Crc == samplerCrc)
{
id = samplers[i].Id;
found = true;
break;
}
}
if (!found)
return;
var material = (Structs.Material*)Material;
for (var i = 0; i < material->TextureCount; ++i)
{
if (material->Textures[i].Id == id)
{
material->Textures[i].SamplerFlags = (samplerFlags & 0xFFFFFDFF) | 0x000001C0;
break;
}
}
}
protected override bool IsStillValid()
{
if (!base.IsStillValid())
return false;
var mtrlHandle = Material->MaterialResourceHandle;
if (mtrlHandle == null)
return false;
var shpkHandle = ((Structs.MtrlResource*)mtrlHandle)->ShpkResourceHandle;
if (shpkHandle == null)
return false;
if (_shaderPackage != shpkHandle->ShaderPackage)
return false;
return true;
}
}

View file

@ -0,0 +1,70 @@
using System;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
namespace Penumbra.Interop.MaterialPreview;
public abstract unsafe class LiveMaterialPreviewerBase : IDisposable
{
private readonly IObjectTable _objects;
public readonly MaterialInfo MaterialInfo;
public readonly CharacterBase* DrawObject;
protected readonly Material* Material;
protected bool Valid;
public LiveMaterialPreviewerBase(IObjectTable objects, MaterialInfo materialInfo)
{
_objects = objects;
MaterialInfo = materialInfo;
var gameObject = MaterialInfo.GetCharacter(objects);
if (gameObject == nint.Zero)
throw new InvalidOperationException("Cannot retrieve game object.");
DrawObject = (CharacterBase*)MaterialInfo.GetDrawObject(gameObject);
if (DrawObject == null)
throw new InvalidOperationException("Cannot retrieve draw object.");
Material = MaterialInfo.GetDrawObjectMaterial(DrawObject);
if (Material == null)
throw new InvalidOperationException("Cannot retrieve material.");
Valid = true;
}
public void Dispose()
{
if (Valid)
Clear(true, IsStillValid());
}
public bool CheckValidity()
{
if (Valid && !IsStillValid())
Clear(false, false);
return Valid;
}
protected virtual void Clear(bool disposing, bool reset)
{
Valid = false;
}
protected virtual bool IsStillValid()
{
var gameObject = MaterialInfo.GetCharacter(_objects);
if (gameObject == nint.Zero)
return false;
if ((nint)DrawObject != MaterialInfo.GetDrawObject(gameObject))
return false;
if (Material != MaterialInfo.GetDrawObjectMaterial(DrawObject))
return false;
return true;
}
}

View file

@ -0,0 +1,120 @@
using System;
using System.Collections.Generic;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Penumbra.Interop.ResourceTree;
using Penumbra.String;
namespace Penumbra.Interop.MaterialPreview;
public enum DrawObjectType
{
PlayerCharacter,
PlayerMainhand,
PlayerOffhand,
PlayerVfx,
MinionCharacter,
MinionUnk1,
MinionUnk2,
MinionUnk3,
};
public readonly record struct MaterialInfo(DrawObjectType Type, int ModelSlot, int MaterialSlot)
{
public nint GetCharacter(IObjectTable objects)
=> GetCharacter(Type, objects);
public static nint GetCharacter(DrawObjectType type, IObjectTable objects)
=> type switch
{
DrawObjectType.PlayerCharacter => objects.GetObjectAddress(0),
DrawObjectType.PlayerMainhand => objects.GetObjectAddress(0),
DrawObjectType.PlayerOffhand => objects.GetObjectAddress(0),
DrawObjectType.PlayerVfx => objects.GetObjectAddress(0),
DrawObjectType.MinionCharacter => objects.GetObjectAddress(1),
DrawObjectType.MinionUnk1 => objects.GetObjectAddress(1),
DrawObjectType.MinionUnk2 => objects.GetObjectAddress(1),
DrawObjectType.MinionUnk3 => objects.GetObjectAddress(1),
_ => nint.Zero,
};
public nint GetDrawObject(nint address)
=> GetDrawObject(Type, address);
public static nint GetDrawObject(DrawObjectType type, IObjectTable objects)
=> GetDrawObject(type, GetCharacter(type, objects));
public static unsafe nint GetDrawObject(DrawObjectType type, nint address)
{
var gameObject = (Character*)address;
if (gameObject == null)
return nint.Zero;
return type switch
{
DrawObjectType.PlayerCharacter => (nint)gameObject->GameObject.GetDrawObject(),
DrawObjectType.PlayerMainhand => *((nint*)&gameObject->DrawData.MainHand + 1),
DrawObjectType.PlayerOffhand => *((nint*)&gameObject->DrawData.OffHand + 1),
DrawObjectType.PlayerVfx => *((nint*)&gameObject->DrawData.UnkF0 + 1),
DrawObjectType.MinionCharacter => (nint)gameObject->GameObject.GetDrawObject(),
DrawObjectType.MinionUnk1 => *((nint*)&gameObject->DrawData.MainHand + 1),
DrawObjectType.MinionUnk2 => *((nint*)&gameObject->DrawData.OffHand + 1),
DrawObjectType.MinionUnk3 => *((nint*)&gameObject->DrawData.UnkF0 + 1),
_ => nint.Zero,
};
}
public unsafe Material* GetDrawObjectMaterial(CharacterBase* drawObject)
{
if (drawObject == null)
return null;
if (ModelSlot < 0 || ModelSlot >= drawObject->SlotCount)
return null;
var model = drawObject->Models[ModelSlot];
if (model == null)
return null;
if (MaterialSlot < 0 || MaterialSlot >= model->MaterialCount)
return null;
return model->Materials[MaterialSlot];
}
public static unsafe List<MaterialInfo> FindMaterials(IObjectTable objects, string materialPath)
{
var needle = ByteString.FromString(materialPath.Replace('\\', '/'), out var m, true) ? m : ByteString.Empty;
var result = new List<MaterialInfo>(Enum.GetValues<DrawObjectType>().Length);
foreach (var type in Enum.GetValues<DrawObjectType>())
{
var drawObject = (CharacterBase*)GetDrawObject(type, objects);
if (drawObject == null)
continue;
for (var i = 0; i < drawObject->SlotCount; ++i)
{
var model = drawObject->Models[i];
if (model == null)
continue;
for (var j = 0; j < model->MaterialCount; ++j)
{
var material = model->Materials[j];
if (material == null)
continue;
var mtrlHandle = material->MaterialResourceHandle;
var path = ResolveContext.GetResourceHandlePath((Structs.ResourceHandle*)mtrlHandle);
if (path == needle)
result.Add(new MaterialInfo(type, i, j));
}
}
}
return result;
}
}

View file

@ -65,26 +65,13 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
private ResourceNode CreateNodeFromGamePath(ResourceType type, nint sourceAddress, Utf8GamePath gamePath, bool @internal)
=> new(null, type, sourceAddress, gamePath, FilterFullPath(Collection.ResolvePath(gamePath) ?? new FullPath(gamePath)), @internal);
private unsafe ResourceNode? CreateNodeFromResourceHandle(ResourceType type, nint sourceAddress, ResourceHandle* handle, bool @internal,
bool withName)
private unsafe ResourceNode? CreateNodeFromResourceHandle(ResourceType type, nint sourceAddress, ResourceHandle* handle, bool @internal,
bool withName)
{
if (handle == null)
return null;
var name = handle->FileName();
if (name.IsEmpty)
return null;
if (name[0] == (byte)'|')
{
var pos = name.IndexOf((byte)'|', 1);
if (pos < 0)
return null;
name = name.Substring(pos + 1);
}
var fullPath = new FullPath(Utf8GamePath.FromByteString(name, out var p) ? p : Utf8GamePath.Empty);
var fullPath = Utf8GamePath.FromByteString(GetResourceHandlePath(handle), out var p) ? new FullPath(p) : FullPath.Empty;
if (fullPath.InternalName.IsEmpty)
return null;
var gamePaths = Collection.ReverseResolvePath(fullPath).ToList();
fullPath = FilterFullPath(fullPath);
@ -161,7 +148,7 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
if (mtrl == null)
return null;
var resource = (MtrlResource*)mtrl->ResourceHandle;
var resource = mtrl->ResourceHandle;
var node = CreateNodeFromResourceHandle(ResourceType.Mtrl, (nint) mtrl, &resource->Handle, false, WithNames);
if (node == null)
return null;
@ -182,7 +169,7 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
if (WithNames)
{
var name = samplers != null && i < samplers.Count ? samplers[i].Item2?.Name : null;
var name = samplers != null && i < samplers.Length ? samplers[i].ShpkSampler?.Name : null;
node.Children.Add(texNode.WithName(name ?? $"Texture #{i}"));
}
else
@ -286,4 +273,25 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
var i = index.GetOffset(array.Length);
return i >= 0 && i < array.Length ? array[i] : null;
}
internal static unsafe ByteString GetResourceHandlePath(ResourceHandle* handle)
{
if (handle == null)
return ByteString.Empty;
var name = handle->FileName();
if (name.IsEmpty)
return ByteString.Empty;
if (name[0] == (byte)'|')
{
var pos = name.IndexOf((byte)'|', 1);
if (pos < 0)
return ByteString.Empty;
name = name.Substring(pos + 1);
}
return name;
}
}

View file

@ -0,0 +1,15 @@
using System.Runtime.InteropServices;
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
namespace Penumbra.Interop.Structs;
[StructLayout( LayoutKind.Explicit )]
public unsafe struct CharacterBaseExt
{
[FieldOffset( 0x0 )]
public CharacterBase CharacterBase;
[FieldOffset( 0x258 )]
public Texture** ColorSetTextures;
}

View file

@ -0,0 +1,31 @@
using System;
using System.Runtime.InteropServices;
namespace Penumbra.Interop.Structs;
[StructLayout(LayoutKind.Explicit, Size = 0x70)]
public unsafe struct ConstantBuffer
{
[FieldOffset(0x20)]
public int Size;
[FieldOffset(0x24)]
public int Flags;
[FieldOffset(0x28)]
private void* _maybeSourcePointer;
public bool TryGetBuffer(out Span<float> buffer)
{
if ((Flags & 0x4003) == 0 && _maybeSourcePointer != null)
{
buffer = new Span<float>(_maybeSourcePointer, Size >> 2);
return true;
}
else
{
buffer = null;
return false;
}
}
}

View file

@ -9,6 +9,9 @@ public unsafe struct HumanExt
[FieldOffset( 0x0 )]
public Human Human;
[FieldOffset( 0x0 )]
public CharacterBaseExt CharacterBase;
[FieldOffset( 0x9E8 )]
public ResourceHandle* Decal;

View file

@ -3,17 +3,42 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
namespace Penumbra.Interop.Structs;
[StructLayout( LayoutKind.Explicit )]
[StructLayout( LayoutKind.Explicit, Size = 0x40 )]
public unsafe struct Material
{
[FieldOffset( 0x10 )]
public ResourceHandle* ResourceHandle;
public MtrlResource* ResourceHandle;
[FieldOffset( 0x18 )]
public uint ShaderPackageFlags;
[FieldOffset( 0x20 )]
public uint* ShaderKeys;
public int ShaderKeyCount
=> (int)((uint*)Textures - ShaderKeys);
[FieldOffset( 0x28 )]
public void* MaterialData;
public ConstantBuffer* MaterialParameter;
[FieldOffset( 0x30 )]
public void** Textures;
public TextureEntry* Textures;
public Texture* Texture( int index ) => ( Texture* )Textures[3 * index + 1];
[FieldOffset( 0x38 )]
public ushort TextureCount;
public Texture* Texture( int index ) => Textures[index].ResourceHandle->KernelTexture;
[StructLayout( LayoutKind.Explicit, Size = 0x18 )]
public struct TextureEntry
{
[FieldOffset( 0x00 )]
public uint Id;
[FieldOffset( 0x08 )]
public TextureResourceHandle* ResourceHandle;
[FieldOffset( 0x10 )]
public uint SamplerFlags;
}
}

View file

@ -0,0 +1,19 @@
using System.Runtime.InteropServices;
namespace Penumbra.Interop.Structs;
public static class ShaderPackageUtility
{
[StructLayout(LayoutKind.Explicit, Size = 0xC)]
public unsafe struct Sampler
{
[FieldOffset(0x0)]
public uint Crc;
[FieldOffset(0x4)]
public uint Id;
[FieldOffset(0xA)]
public ushort Slot;
}
}

View file

@ -0,0 +1,36 @@
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
namespace Penumbra.Interop.Structs;
public unsafe static class TextureUtility
{
private static readonly Functions Funcs = new();
public static Texture* Create2D(Device* device, int* size, byte mipLevel, uint textureFormat, uint flags, uint unk)
=> ((delegate* unmanaged<Device*, int*, byte, uint, uint, uint, Texture*>)Funcs.TextureCreate2D)(device, size, mipLevel, textureFormat, flags, unk);
public static bool InitializeContents(Texture* texture, void* contents)
=> ((delegate* unmanaged<Texture*, void*, bool>)Funcs.TextureInitializeContents)(texture, contents);
public static void IncRef(Texture* texture)
=> ((delegate* unmanaged<Texture*, void>)(*(void***)texture)[2])(texture);
public static void DecRef(Texture* texture)
=> ((delegate* unmanaged<Texture*, void>)(*(void***)texture)[3])(texture);
private sealed class Functions
{
[Signature("E8 ?? ?? ?? ?? 8B 0F 48 8D 54 24")]
public nint TextureCreate2D = nint.Zero;
[Signature("E9 ?? ?? ?? ?? 8B 02 25")]
public nint TextureInitializeContents = nint.Zero;
public Functions()
{
SignatureHelper.Initialise(this);
}
}
}

View file

@ -18,7 +18,7 @@ using Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow;
public class FileEditor<T> where T : class, IWritable
public class FileEditor<T> : IDisposable where T : class, IWritable
{
private readonly FileDialogService _fileDialog;
private readonly IDataManager _gameData;
@ -26,7 +26,7 @@ public class FileEditor<T> where T : class, IWritable
public FileEditor(ModEditWindow owner, IDataManager gameData, Configuration config, FileDialogService fileDialog, string tabName,
string fileType, Func<IReadOnlyList<FileRegistry>> getFiles, Func<T, bool, bool> drawEdit, Func<string> getInitialPath,
Func<byte[], T?> parseFile)
Func<byte[], string, bool, T?> parseFile)
{
_owner = owner;
_gameData = gameData;
@ -60,11 +60,19 @@ public class FileEditor<T> where T : class, IWritable
DrawFilePanel();
}
private readonly string _tabName;
private readonly string _fileType;
private readonly Func<T, bool, bool> _drawEdit;
private readonly Func<string> _getInitialPath;
private readonly Func<byte[], T?> _parseFile;
public void Dispose()
{
(_currentFile as IDisposable)?.Dispose();
_currentFile = null;
(_defaultFile as IDisposable)?.Dispose();
_defaultFile = null;
}
private readonly string _tabName;
private readonly string _fileType;
private readonly Func<T, bool, bool> _drawEdit;
private readonly Func<string> _getInitialPath;
private readonly Func<byte[], string, bool, T?> _parseFile;
private FileRegistry? _currentPath;
private T? _currentFile;
@ -99,7 +107,9 @@ public class FileEditor<T> where T : class, IWritable
if (file != null)
{
_defaultException = null;
_defaultFile = _parseFile(file.Data);
(_defaultFile as IDisposable)?.Dispose();
_defaultFile = null; // Avoid double disposal if an exception occurs during the parsing of the new file.
_defaultFile = _parseFile(file.Data, _defaultPath, false);
}
else
{
@ -158,6 +168,7 @@ public class FileEditor<T> where T : class, IWritable
{
_currentException = null;
_currentPath = null;
(_currentFile as IDisposable)?.Dispose();
_currentFile = null;
_changed = false;
}
@ -181,10 +192,13 @@ public class FileEditor<T> where T : class, IWritable
try
{
var bytes = File.ReadAllBytes(_currentPath.File.FullName);
_currentFile = _parseFile(bytes);
(_currentFile as IDisposable)?.Dispose();
_currentFile = null; // Avoid double disposal if an exception occurs during the parsing of the new file.
_currentFile = _parseFile(bytes, _currentPath.File.FullName, true);
}
catch (Exception e)
{
(_currentFile as IDisposable)?.Dispose();
_currentFile = null;
_currentException = e;
}

View file

@ -13,57 +13,77 @@ namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow
{
private bool DrawMaterialColorSetChange( MtrlFile file, bool disabled )
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 DrawMaterialColorSetChange(MtrlTab tab, bool disabled)
{
if( !file.ColorSets.Any( c => c.HasRows ) )
{
if (!tab.SamplerIds.Contains(ShpkFile.TableSamplerId) || !tab.Mtrl.ColorSets.Any(c => c.HasRows))
return false;
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
if (!ImGui.CollapsingHeader("Color Set", ImGuiTreeNodeFlags.DefaultOpen))
return false;
var hasAnyDye = tab.UseColorDyeSet;
ColorSetCopyAllClipboardButton(tab.Mtrl, 0);
ImGui.SameLine();
var ret = ColorSetPasteAllClipboardButton(tab, 0, disabled);
if (!disabled)
{
ImGui.SameLine();
ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0));
ImGui.SameLine();
ret |= ColorSetDyeableCheckbox(tab, ref hasAnyDye);
}
ColorSetCopyAllClipboardButton( file, 0 );
ImGui.SameLine();
var ret = ColorSetPasteAllClipboardButton( file, 0 );
ImGui.SameLine();
ImGui.Dummy( ImGuiHelpers.ScaledVector2( 20, 0 ) );
ImGui.SameLine();
ret |= DrawPreviewDye( file, disabled );
using var table = ImRaii.Table( "##ColorSets", 11,
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV );
if( !table )
if (hasAnyDye)
{
return false;
ImGui.SameLine();
ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0));
ImGui.SameLine();
ret |= DrawPreviewDye(tab, disabled);
}
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" );
ImGui.TableNextColumn();
ImGui.TableHeader( "Dye" );
ImGui.TableNextColumn();
ImGui.TableHeader( "Dye Preview" );
using var table = ImRaii.Table("##ColorSets", hasAnyDye ? 11 : 9,
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV);
if (!table)
return false;
for( var j = 0; j < file.ColorSets.Length; ++j )
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 (hasAnyDye)
{
using var _ = ImRaii.PushId( j );
for( var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i )
ImGui.TableNextColumn();
ImGui.TableHeader("Dye");
ImGui.TableNextColumn();
ImGui.TableHeader("Dye Preview");
}
for (var j = 0; j < tab.Mtrl.ColorSets.Length; ++j)
{
using var _ = ImRaii.PushId(j);
for (var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i)
{
ret |= DrawColorSetRow( file, j, i, disabled );
ret |= DrawColorSetRow(tab, j, i, disabled, hasAnyDye);
ImGui.TableNextRow();
}
}
@ -72,22 +92,20 @@ public partial class ModEditWindow
}
private static void ColorSetCopyAllClipboardButton( MtrlFile file, int colorSetIdx )
private static void ColorSetCopyAllClipboardButton(MtrlFile file, int colorSetIdx)
{
if( !ImGui.Button( "Export All Rows to Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ) ) )
{
if (!ImGui.Button("Export All Rows to Clipboard", ImGuiHelpers.ScaledVector2(200, 0)))
return;
}
try
{
var data1 = file.ColorSets[ colorSetIdx ].Rows.AsBytes();
var data2 = file.ColorDyeSets.Length > colorSetIdx ? file.ColorDyeSets[ colorSetIdx ].Rows.AsBytes() : ReadOnlySpan< byte >.Empty;
var data1 = file.ColorSets[colorSetIdx].Rows.AsBytes();
var data2 = file.ColorDyeSets.Length > colorSetIdx ? file.ColorDyeSets[colorSetIdx].Rows.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 );
data1.TryCopyTo(array);
data2.TryCopyTo(array.AsSpan(data1.Length));
var text = Convert.ToBase64String(array);
ImGui.SetClipboardText(text);
}
catch
{
@ -95,61 +113,64 @@ public partial class ModEditWindow
}
}
private bool DrawPreviewDye( MtrlFile file, bool disabled )
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 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;
for( var j = 0; j < file.ColorDyeSets.Length; ++j )
for (var j = 0; j < tab.Mtrl.ColorDyeSets.Length; ++j)
{
for( var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i )
{
ret |= file.ApplyDyeTemplate( _stainService.StmFile, j, i, dyeId );
}
for (var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i)
ret |= tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, j, i, dyeId);
}
tab.UpdateColorSetPreview();
return ret;
}
ImGui.SameLine();
var label = dyeId == 0 ? "Preview Dye###previewDye" : $"{name} (Preview)###previewDye";
_stainService.StainCombo.Draw( label, dyeColor, string.Empty, true, gloss);
if (_stainService.StainCombo.Draw(label, dyeColor, string.Empty, true, gloss))
tab.UpdateColorSetPreview();
return false;
}
private static unsafe bool ColorSetPasteAllClipboardButton( MtrlFile file, int colorSetIdx )
private static unsafe bool ColorSetPasteAllClipboardButton(MtrlTab tab, int colorSetIdx, bool disabled)
{
if( !ImGui.Button( "Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ) ) || file.ColorSets.Length <= colorSetIdx )
{
if (!ImGuiUtil.DrawDisabledButton("Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2(200, 0), string.Empty, disabled)
|| tab.Mtrl.ColorSets.Length <= colorSetIdx)
return false;
}
try
{
var text = ImGui.GetClipboardText();
var data = Convert.FromBase64String( text );
if( data.Length < Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() )
{
var data = Convert.FromBase64String(text);
if (data.Length < Marshal.SizeOf<MtrlFile.ColorSet.RowArray>())
return false;
}
ref var rows = ref file.ColorSets[ colorSetIdx ].Rows;
fixed( void* ptr = data, output = &rows )
ref var rows = ref tab.Mtrl.ColorSets[colorSetIdx].Rows;
fixed (void* ptr = data, output = &rows)
{
MemoryUtility.MemCpyUnchecked( output, ptr, Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() );
if( data.Length >= Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() + Marshal.SizeOf< MtrlFile.ColorDyeSet.RowArray >()
&& file.ColorDyeSets.Length > colorSetIdx )
MemoryUtility.MemCpyUnchecked(output, ptr, Marshal.SizeOf<MtrlFile.ColorSet.RowArray>());
if (data.Length >= Marshal.SizeOf<MtrlFile.ColorSet.RowArray>() + Marshal.SizeOf<MtrlFile.ColorDyeSet.RowArray>()
&& tab.Mtrl.ColorDyeSets.Length > colorSetIdx)
{
ref var dyeRows = ref file.ColorDyeSets[ colorSetIdx ].Rows;
fixed( void* output2 = &dyeRows )
ref var dyeRows = ref tab.Mtrl.ColorDyeSets[colorSetIdx].Rows;
fixed (void* output2 = &dyeRows)
{
MemoryUtility.MemCpyUnchecked( output2, ( byte* )ptr + Marshal.SizeOf< MtrlFile.ColorSet.RowArray >(), Marshal.SizeOf< MtrlFile.ColorDyeSet.RowArray >() );
MemoryUtility.MemCpyUnchecked(output2, (byte*)ptr + Marshal.SizeOf<MtrlFile.ColorSet.RowArray>(),
Marshal.SizeOf<MtrlFile.ColorDyeSet.RowArray>());
}
}
}
tab.UpdateColorSetPreview();
return true;
}
catch
@ -158,279 +179,372 @@ public partial class ModEditWindow
}
}
private static unsafe void ColorSetCopyClipboardButton( MtrlFile.ColorSet.Row row, MtrlFile.ColorDyeSet.Row dye )
private static unsafe void ColorSetCopyClipboardButton(MtrlFile.ColorSet.Row row, MtrlFile.ColorDyeSet.Row dye)
{
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Clipboard.ToIconString(), ImGui.GetFrameHeight() * Vector2.One,
"Export this row to your clipboard.", false, true ) )
{
try
{
var data = new byte[MtrlFile.ColorSet.Row.Size + 2];
fixed( byte* ptr = data )
{
MemoryUtility.MemCpyUnchecked( ptr, &row, MtrlFile.ColorSet.Row.Size );
MemoryUtility.MemCpyUnchecked( ptr + MtrlFile.ColorSet.Row.Size, &dye, 2 );
}
if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), ImGui.GetFrameHeight() * Vector2.One,
"Export this row to your clipboard.", false, true))
return;
var text = Convert.ToBase64String( data );
ImGui.SetClipboardText( text );
}
catch
try
{
var data = new byte[MtrlFile.ColorSet.Row.Size + 2];
fixed (byte* ptr = data)
{
// ignored
MemoryUtility.MemCpyUnchecked(ptr, &row, MtrlFile.ColorSet.Row.Size);
MemoryUtility.MemCpyUnchecked(ptr + MtrlFile.ColorSet.Row.Size, &dye, 2);
}
var text = Convert.ToBase64String(data);
ImGui.SetClipboardText(text);
}
catch
{
// ignored
}
}
private static unsafe bool ColorSetPasteFromClipboardButton( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled )
private static bool ColorSetDyeableCheckbox(MtrlTab tab, ref bool dyeable)
{
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Paste.ToIconString(), ImGui.GetFrameHeight() * Vector2.One,
"Import an exported row from your clipboard onto this row.", disabled, true ) )
var ret = ImGui.Checkbox("Dyeable", ref dyeable);
if (ret)
{
try
{
var text = ImGui.GetClipboardText();
var data = Convert.FromBase64String( text );
if( data.Length != MtrlFile.ColorSet.Row.Size + 2
|| file.ColorSets.Length <= colorSetIdx )
{
return false;
}
fixed( byte* ptr = data )
{
file.ColorSets[ colorSetIdx ].Rows[ rowIdx ] = *( MtrlFile.ColorSet.Row* )ptr;
if( colorSetIdx < file.ColorDyeSets.Length )
{
file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] = *( MtrlFile.ColorDyeSet.Row* )( ptr + MtrlFile.ColorSet.Row.Size );
}
}
return true;
}
catch
{
// ignored
}
tab.UseColorDyeSet = dyeable;
if (dyeable)
tab.Mtrl.FindOrAddColorDyeSet();
tab.UpdateColorSetPreview();
}
return false;
return ret;
}
private bool DrawColorSetRow( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled )
private static unsafe bool ColorSetPasteFromClipboardButton(MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled)
{
static bool FixFloat( ref float val, float current )
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
{
val = ( float )( Half )val;
var text = ImGui.GetClipboardText();
var data = Convert.FromBase64String(text);
if (data.Length != MtrlFile.ColorSet.Row.Size + 2
|| tab.Mtrl.ColorSets.Length <= colorSetIdx)
return false;
fixed (byte* ptr = data)
{
tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx] = *(MtrlFile.ColorSet.Row*)ptr;
if (colorSetIdx < tab.Mtrl.ColorDyeSets.Length)
tab.Mtrl.ColorDyeSets[colorSetIdx].Rows[rowIdx] = *(MtrlFile.ColorDyeSet.Row*)(ptr + MtrlFile.ColorSet.Row.Size);
}
tab.UpdateColorSetRowPreview(rowIdx);
return true;
}
catch
{
return false;
}
}
private static void ColorSetHighlightButton(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.ColorSetPreviewers.Count == 0, true);
if (ImGui.IsItemHovered())
tab.HighlightColorSetRow(rowIdx);
else if (tab.HighlightedColorSetRow == rowIdx)
tab.CancelColorSetHighlight();
}
private bool DrawColorSetRow(MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled, bool hasAnyDye)
{
static bool FixFloat(ref float val, float current)
{
val = (float)(Half)val;
return val != current;
}
using var id = ImRaii.PushId( rowIdx );
var row = file.ColorSets[ colorSetIdx ].Rows[ rowIdx ];
var hasDye = file.ColorDyeSets.Length > colorSetIdx;
var dye = hasDye ? file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] : new MtrlFile.ColorDyeSet.Row();
using var id = ImRaii.PushId(rowIdx);
var row = tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx];
var hasDye = hasAnyDye && tab.Mtrl.ColorDyeSets.Length > colorSetIdx;
var dye = hasDye ? tab.Mtrl.ColorDyeSets[colorSetIdx].Rows[rowIdx] : new MtrlFile.ColorDyeSet.Row();
var floatSize = 70 * UiHelpers.Scale;
var intSize = 45 * UiHelpers.Scale;
ImGui.TableNextColumn();
ColorSetCopyClipboardButton( row, dye );
ColorSetCopyClipboardButton(row, dye);
ImGui.SameLine();
var ret = ColorSetPasteFromClipboardButton( file, colorSetIdx, rowIdx, disabled );
var ret = ColorSetPasteFromClipboardButton(tab, colorSetIdx, rowIdx, disabled);
ImGui.SameLine();
ColorSetHighlightButton(tab, rowIdx, disabled);
ImGui.TableNextColumn();
ImGui.TextUnformatted( $"#{rowIdx + 1:D2}" );
ImGui.TextUnformatted($"#{rowIdx + 1:D2}");
ImGui.TableNextColumn();
using var dis = ImRaii.Disabled( disabled );
ret |= ColorPicker( "##Diffuse", "Diffuse Color", row.Diffuse, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Diffuse = c );
if( hasDye )
using var dis = ImRaii.Disabled(disabled);
ret |= ColorPicker("##Diffuse", "Diffuse Color", row.Diffuse, c =>
{
tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].Diffuse = c;
tab.UpdateColorSetRowPreview(rowIdx);
});
if (hasDye)
{
ImGui.SameLine();
ret |= ImGuiUtil.Checkbox( "##dyeDiffuse", "Apply Diffuse Color on Dye", dye.Diffuse,
b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Diffuse = b, ImGuiHoveredFlags.AllowWhenDisabled );
ret |= ImGuiUtil.Checkbox("##dyeDiffuse", "Apply Diffuse Color on Dye", dye.Diffuse,
b =>
{
tab.Mtrl.ColorDyeSets[colorSetIdx].Rows[rowIdx].Diffuse = b;
tab.UpdateColorSetRowPreview(rowIdx);
}, ImGuiHoveredFlags.AllowWhenDisabled);
}
ImGui.TableNextColumn();
ret |= ColorPicker( "##Specular", "Specular Color", row.Specular, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Specular = c );
ret |= ColorPicker("##Specular", "Specular Color", row.Specular, c =>
{
tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].Specular = c;
tab.UpdateColorSetRowPreview(rowIdx);
});
ImGui.SameLine();
var tmpFloat = row.SpecularStrength;
ImGui.SetNextItemWidth( floatSize );
if( ImGui.DragFloat( "##SpecularStrength", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.SpecularStrength ) )
ImGui.SetNextItemWidth(floatSize);
if (ImGui.DragFloat("##SpecularStrength", ref tmpFloat, 0.1f, 0f, HalfMaxValue, "%.2f") && FixFloat(ref tmpFloat, row.SpecularStrength))
{
file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = tmpFloat;
tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].SpecularStrength = tmpFloat;
ret = true;
tab.UpdateColorSetRowPreview(rowIdx);
}
ImGuiUtil.HoverTooltip( "Specular Strength", ImGuiHoveredFlags.AllowWhenDisabled );
ImGuiUtil.HoverTooltip("Specular Strength", ImGuiHoveredFlags.AllowWhenDisabled);
if( hasDye )
if (hasDye)
{
ImGui.SameLine();
ret |= ImGuiUtil.Checkbox( "##dyeSpecular", "Apply Specular Color on Dye", dye.Specular,
b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Specular = b, ImGuiHoveredFlags.AllowWhenDisabled );
ret |= ImGuiUtil.Checkbox("##dyeSpecular", "Apply Specular Color on Dye", dye.Specular,
b =>
{
tab.Mtrl.ColorDyeSets[colorSetIdx].Rows[rowIdx].Specular = b;
tab.UpdateColorSetRowPreview(rowIdx);
}, ImGuiHoveredFlags.AllowWhenDisabled);
ImGui.SameLine();
ret |= ImGuiUtil.Checkbox( "##dyeSpecularStrength", "Apply Specular Strength on Dye", dye.SpecularStrength,
b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = b, ImGuiHoveredFlags.AllowWhenDisabled );
ret |= ImGuiUtil.Checkbox("##dyeSpecularStrength", "Apply Specular Strength on Dye", dye.SpecularStrength,
b =>
{
tab.Mtrl.ColorDyeSets[colorSetIdx].Rows[rowIdx].SpecularStrength = b;
tab.UpdateColorSetRowPreview(rowIdx);
}, ImGuiHoveredFlags.AllowWhenDisabled);
}
ImGui.TableNextColumn();
ret |= ColorPicker( "##Emissive", "Emissive Color", row.Emissive, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Emissive = c );
if( hasDye )
ret |= ColorPicker("##Emissive", "Emissive Color", row.Emissive, c =>
{
tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].Emissive = c;
tab.UpdateColorSetRowPreview(rowIdx);
});
if (hasDye)
{
ImGui.SameLine();
ret |= ImGuiUtil.Checkbox( "##dyeEmissive", "Apply Emissive Color on Dye", dye.Emissive,
b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Emissive = b, ImGuiHoveredFlags.AllowWhenDisabled );
ret |= ImGuiUtil.Checkbox("##dyeEmissive", "Apply Emissive Color on Dye", dye.Emissive,
b =>
{
tab.Mtrl.ColorDyeSets[colorSetIdx].Rows[rowIdx].Emissive = b;
tab.UpdateColorSetRowPreview(rowIdx);
}, ImGuiHoveredFlags.AllowWhenDisabled);
}
ImGui.TableNextColumn();
tmpFloat = row.GlossStrength;
ImGui.SetNextItemWidth( floatSize );
if( ImGui.DragFloat( "##GlossStrength", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.GlossStrength ) )
ImGui.SetNextItemWidth(floatSize);
if (ImGui.DragFloat("##GlossStrength", ref tmpFloat, Math.Max(0.1f, tmpFloat * 0.025f), HalfEpsilon, HalfMaxValue, "%.1f")
&& FixFloat(ref tmpFloat, row.GlossStrength))
{
file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].GlossStrength = tmpFloat;
tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].GlossStrength = Math.Max(tmpFloat, HalfEpsilon);
ret = true;
tab.UpdateColorSetRowPreview(rowIdx);
}
ImGuiUtil.HoverTooltip( "Gloss Strength", ImGuiHoveredFlags.AllowWhenDisabled );
if( hasDye )
ImGuiUtil.HoverTooltip("Gloss Strength", ImGuiHoveredFlags.AllowWhenDisabled);
if (hasDye)
{
ImGui.SameLine();
ret |= ImGuiUtil.Checkbox( "##dyeGloss", "Apply Gloss Strength on Dye", dye.Gloss,
b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Gloss = b, ImGuiHoveredFlags.AllowWhenDisabled );
ret |= ImGuiUtil.Checkbox("##dyeGloss", "Apply Gloss Strength on Dye", dye.Gloss,
b =>
{
tab.Mtrl.ColorDyeSets[colorSetIdx].Rows[rowIdx].Gloss = b;
tab.UpdateColorSetRowPreview(rowIdx);
}, ImGuiHoveredFlags.AllowWhenDisabled);
}
ImGui.TableNextColumn();
int tmpInt = row.TileSet;
ImGui.SetNextItemWidth( intSize );
if( ImGui.InputInt( "##TileSet", ref tmpInt, 0, 0 ) && tmpInt != row.TileSet && tmpInt is >= 0 and <= ushort.MaxValue )
ImGui.SetNextItemWidth(intSize);
if (ImGui.DragInt("##TileSet", ref tmpInt, 0.25f, 0, 63) && tmpInt != row.TileSet && tmpInt is >= 0 and <= ushort.MaxValue)
{
file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].TileSet = ( ushort )tmpInt;
tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].TileSet = (ushort)Math.Clamp(tmpInt, 0, 63);
ret = true;
tab.UpdateColorSetRowPreview(rowIdx);
}
ImGuiUtil.HoverTooltip( "Tile Set", ImGuiHoveredFlags.AllowWhenDisabled );
ImGuiUtil.HoverTooltip("Tile Set", ImGuiHoveredFlags.AllowWhenDisabled);
ImGui.TableNextColumn();
tmpFloat = row.MaterialRepeat.X;
ImGui.SetNextItemWidth( floatSize );
if( ImGui.DragFloat( "##RepeatX", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialRepeat.X ) )
ImGui.SetNextItemWidth(floatSize);
if (ImGui.DragFloat("##RepeatX", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f")
&& FixFloat(ref tmpFloat, row.MaterialRepeat.X))
{
file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { X = tmpFloat };
tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].MaterialRepeat = row.MaterialRepeat with { X = tmpFloat };
ret = true;
tab.UpdateColorSetRowPreview(rowIdx);
}
ImGuiUtil.HoverTooltip( "Repeat X", ImGuiHoveredFlags.AllowWhenDisabled );
ImGuiUtil.HoverTooltip("Repeat X", ImGuiHoveredFlags.AllowWhenDisabled);
ImGui.SameLine();
tmpFloat = row.MaterialRepeat.Y;
ImGui.SetNextItemWidth( floatSize );
if( ImGui.DragFloat( "##RepeatY", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialRepeat.Y ) )
ImGui.SetNextItemWidth(floatSize);
if (ImGui.DragFloat("##RepeatY", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f")
&& FixFloat(ref tmpFloat, row.MaterialRepeat.Y))
{
file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat };
tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat };
ret = true;
tab.UpdateColorSetRowPreview(rowIdx);
}
ImGuiUtil.HoverTooltip( "Repeat Y", ImGuiHoveredFlags.AllowWhenDisabled );
ImGuiUtil.HoverTooltip("Repeat Y", ImGuiHoveredFlags.AllowWhenDisabled);
ImGui.TableNextColumn();
tmpFloat = row.MaterialSkew.X;
ImGui.SetNextItemWidth( floatSize );
if( ImGui.DragFloat( "##SkewX", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialSkew.X ) )
ImGui.SetNextItemWidth(floatSize);
if (ImGui.DragFloat("##SkewX", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") && FixFloat(ref tmpFloat, row.MaterialSkew.X))
{
file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { X = tmpFloat };
tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].MaterialSkew = row.MaterialSkew with { X = tmpFloat };
ret = true;
tab.UpdateColorSetRowPreview(rowIdx);
}
ImGuiUtil.HoverTooltip( "Skew X", ImGuiHoveredFlags.AllowWhenDisabled );
ImGuiUtil.HoverTooltip("Skew X", ImGuiHoveredFlags.AllowWhenDisabled);
ImGui.SameLine();
tmpFloat = row.MaterialSkew.Y;
ImGui.SetNextItemWidth( floatSize );
if( ImGui.DragFloat( "##SkewY", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialSkew.Y ) )
ImGui.SetNextItemWidth(floatSize);
if (ImGui.DragFloat("##SkewY", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") && FixFloat(ref tmpFloat, row.MaterialSkew.Y))
{
file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { Y = tmpFloat };
tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].MaterialSkew = row.MaterialSkew with { Y = tmpFloat };
ret = true;
tab.UpdateColorSetRowPreview(rowIdx);
}
ImGuiUtil.HoverTooltip( "Skew Y", ImGuiHoveredFlags.AllowWhenDisabled );
ImGuiUtil.HoverTooltip("Skew Y", ImGuiHoveredFlags.AllowWhenDisabled);
ImGui.TableNextColumn();
if( hasDye )
if (hasDye)
{
if(_stainService.TemplateCombo.Draw( "##dyeTemplate", dye.Template.ToString(), string.Empty, intSize
+ ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton ) )
ImGui.TableNextColumn();
if (_stainService.TemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, intSize
+ ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton))
{
file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = _stainService.TemplateCombo.CurrentSelection;
tab.Mtrl.ColorDyeSets[colorSetIdx].Rows[rowIdx].Template = _stainService.TemplateCombo.CurrentSelection;
ret = true;
tab.UpdateColorSetRowPreview(rowIdx);
}
ImGuiUtil.HoverTooltip( "Dye Template", ImGuiHoveredFlags.AllowWhenDisabled );
ImGuiUtil.HoverTooltip("Dye Template", ImGuiHoveredFlags.AllowWhenDisabled);
ImGui.TableNextColumn();
ret |= DrawDyePreview( file, colorSetIdx, rowIdx, disabled, dye, floatSize );
ret |= DrawDyePreview(tab, colorSetIdx, rowIdx, disabled, dye, floatSize);
}
else
else if (hasAnyDye)
{
ImGui.TableNextColumn();
ImGui.TableNextColumn();
}
return ret;
}
private bool DrawDyePreview( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled, MtrlFile.ColorDyeSet.Row dye, float floatSize )
private bool DrawDyePreview(MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled, MtrlFile.ColorDyeSet.Row dye, float floatSize)
{
var stain = _stainService.StainCombo.CurrentSelection.Key;
if( stain == 0 || !_stainService.StmFile.Entries.TryGetValue( dye.Template, out var entry ) )
{
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 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 );
var ret = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2(ImGui.GetFrameHeight()),
"Apply the selected dye to this row.", disabled, true);
ret = ret && file.ApplyDyeTemplate(_stainService.StmFile, colorSetIdx, rowIdx, stain );
ret = ret && tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, colorSetIdx, rowIdx, stain);
if (ret)
tab.UpdateColorSetRowPreview(rowIdx);
ImGui.SameLine();
ColorPicker( "##diffusePreview", string.Empty, values.Diffuse, _ => { }, "D" );
ColorPicker("##diffusePreview", string.Empty, values.Diffuse, _ => { }, "D");
ImGui.SameLine();
ColorPicker( "##specularPreview", string.Empty, values.Specular, _ => { }, "S" );
ColorPicker("##specularPreview", string.Empty, values.Specular, _ => { }, "S");
ImGui.SameLine();
ColorPicker( "##emissivePreview", string.Empty, values.Emissive, _ => { }, "E" );
ColorPicker("##emissivePreview", string.Empty, values.Emissive, _ => { }, "E");
ImGui.SameLine();
using var dis = ImRaii.Disabled();
ImGui.SetNextItemWidth( floatSize );
ImGui.DragFloat( "##gloss", ref values.Gloss, 0, 0, 0, "%.2f G" );
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, 0, 0, "%.2f S" );
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 = "" )
private static bool ColorPicker(string label, string tooltip, Vector3 input, Action<Vector3> setter, string letter = "")
{
var ret = false;
var tmp = input;
if( ImGui.ColorEdit3( label, ref tmp,
ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.DisplayRGB | ImGuiColorEditFlags.InputRGB | ImGuiColorEditFlags.NoTooltip )
&& tmp != input )
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( tmp );
setter(PseudoSquareRgb(tmp));
ret = true;
}
if( letter.Length > 0 && ImGui.IsItemVisible() )
if (letter.Length > 0 && ImGui.IsItemVisible())
{
var textSize = ImGui.CalcTextSize( letter );
var center = ImGui.GetItemRectMin() + ( ImGui.GetItemRectSize() - textSize ) / 2;
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 );
ImGui.GetWindowDrawList().AddText(center, textColor, letter);
}
ImGuiUtil.HoverTooltip( tooltip, ImGuiHoveredFlags.AllowWhenDisabled );
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);
private static Vector3 PseudoSqrtRgb(Vector3 vec)
=> new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z));
private static Vector4 PseudoSqrtRgb(Vector4 vec)
=> new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z), vec.W);
}

View file

@ -0,0 +1,248 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Numerics;
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, float editorWidth);
}
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, float editorWidth)
{
var fieldWidth = (editorWidth - (values.Length - 1) * ImGui.GetStyle().ItemSpacing.X) / values.Length;
var ret = false;
for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx)
{
if (valueIdx > 0)
ImGui.SameLine();
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, float editorWidth)
{
var fieldWidth = (editorWidth - (values.Length - 1) * ImGui.GetStyle().ItemSpacing.X) / values.Length;
var ret = false;
for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx)
{
if (valueIdx > 0)
ImGui.SameLine();
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, float editorWidth)
{
switch (values.Length)
{
case 3:
{
ImGui.SetNextItemWidth(editorWidth);
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:
{
ImGui.SetNextItemWidth(editorWidth);
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, editorWidth);
}
}
}
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, float editorWidth)
{
var fieldWidth = (editorWidth - (values.Length - 1) * ImGui.GetStyle().ItemSpacing.X) / values.Length;
var ret = false;
for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx)
{
using var id = ImRaii.PushId(valueIdx);
if (valueIdx > 0)
ImGui.SameLine();
ImGui.SetNextItemWidth(MathF.Round(fieldWidth * (valueIdx + 1)) - MathF.Round(fieldWidth * valueIdx));
var currentValue = values[valueIdx];
var currentLabel = _values.FirstOrNull(v => v.Value == currentValue)?.Label
?? currentValue.ToString(CultureInfo.CurrentCulture);
ret = disabled
? ImGui.InputText(string.Empty, ref currentLabel, (uint)currentLabel.Length, ImGuiInputTextFlags.ReadOnly)
: DrawCombo(currentLabel, ref values[valueIdx]);
}
return ret;
}
private bool DrawCombo(string label, ref float currentValue)
{
using var c = ImRaii.Combo(string.Empty, label);
if (!c)
return false;
var ret = false;
foreach (var (valueLabel, value, valueDescription) in _values)
{
if (ImGui.Selectable(valueLabel, value == currentValue))
{
currentValue = value;
ret = true;
}
if (valueDescription.Length > 0)
ImGuiUtil.SelectableHelpMarker(valueDescription);
}
return ret;
}
}
}

View file

@ -1,293 +1,783 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Numerics;
using Dalamud.Interface;
using Dalamud.Interface.Internal.Notifications;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using ImGuiNET;
using Newtonsoft.Json.Linq;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii;
using Penumbra.GameData;
using Penumbra.GameData.Data;
using Penumbra.GameData.Files;
using Penumbra.Services;
using Penumbra.GameData.Structs;
using Penumbra.Interop.MaterialPreview;
using Penumbra.String;
using Penumbra.String.Classes;
using Penumbra.Util;
using static Penumbra.GameData.Files.ShpkFile;
namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow
{
private sealed class MtrlTab : IWritable
private sealed class MtrlTab : IWritable, IDisposable
{
private const int ShpkPrefixLength = 16;
private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk/"u8, true, true, true);
private readonly ModEditWindow _edit;
public readonly MtrlFile Mtrl;
public readonly string FilePath;
public readonly bool Writable;
public uint NewKeyId;
public uint NewKeyDefault;
public uint NewConstantId;
public int NewConstantIdx;
public uint NewSamplerId;
public int NewSamplerIdx;
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 ShpkFile? AssociatedShpk;
public readonly List< string > TextureLabels = new(4);
public FullPath LoadedShpkPath = FullPath.Empty;
public string LoadedShpkPathName = string.Empty;
public float TextureLabelWidth;
public readonly string LoadedBaseDevkitPathName;
public readonly JObject? AssociatedBaseDevkit;
// Shader Key State
public readonly List< string > ShaderKeyLabels = new(16);
public readonly Dictionary< uint, uint > DefinedShaderKeys = new(16);
public readonly List< int > MissingShaderKeyIndices = new(16);
public readonly List< uint > AvailableKeyValues = new(16);
public string VertexShaders = "Vertex Shaders: ???";
public string PixelShaders = "Pixel Shaders: ???";
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;
public bool UseColorDyeSet;
// Material Constants
public readonly List< (string Name, bool ComponentOnly, int ParamValueOffset) > MaterialConstants = new(16);
public readonly List< (string Name, uint Id, ushort ByteSize) > MissingMaterialConstants = new(16);
public readonly HashSet< uint > DefinedMaterialConstants = new(16);
public readonly
List<(string Header, List<(string Label, int ConstantIndex, Range Slice, string Description, bool MonoFont, IConstantEditor Editor)>
Constants)> Constants = new(16);
public string MaterialConstantLabel = "Constants###Constants";
public IndexSet OrphanedMaterialValues = new(0, false);
public int AliasedMaterialValueCount;
public bool HasMalformedMaterialConstants;
// Live-Previewers
public readonly List<LiveMaterialPreviewer> MaterialPreviewers = new(4);
public readonly List<LiveColorSetPreviewer> ColorSetPreviewers = new(4);
public int HighlightedColorSetRow = -1;
public readonly Stopwatch HighlightTime = new();
// Samplers
public readonly List< (string Label, string FileName) > Samplers = new(4);
public readonly List< (string Name, uint Id) > MissingSamplers = new(4);
public readonly HashSet< uint > DefinedSamplers = new(4);
public IndexSet OrphanedSamplers = new(0, false);
public int AliasedSamplerCount;
public FullPath FindAssociatedShpk( out string defaultPath, out Utf8GamePath defaultGamePath )
public FullPath FindAssociatedShpk(out string defaultPath, out Utf8GamePath defaultGamePath)
{
defaultPath = GamePaths.Shader.ShpkPath( Mtrl.ShaderPackage.Name );
if( !Utf8GamePath.FromString( defaultPath, out defaultGamePath, true ) )
{
defaultPath = GamePaths.Shader.ShpkPath(Mtrl.ShaderPackage.Name);
if (!Utf8GamePath.FromString(defaultPath, out defaultGamePath, true))
return FullPath.Empty;
}
return _edit.FindBestMatch( defaultGamePath );
return _edit.FindBestMatch(defaultGamePath);
}
public void LoadShpk( FullPath path )
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._dalamud.GameData.GetFile( LoadedShpkPath.InternalName.ToString() )?.Data;
AssociatedShpk = data?.Length > 0 ? new ShpkFile( data ) : throw new Exception( "Failure to load file data." );
? File.ReadAllBytes(LoadedShpkPath.FullName)
: _edit._dalamud.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 )
catch (Exception e)
{
LoadedShpkPath = FullPath.Empty;
LoadedShpkPathName = string.Empty;
AssociatedShpk = null;
Penumbra.Chat.NotificationMessage( $"Could not load {LoadedShpkPath.ToPath()}:\n{e}", "Penumbra Advanced Editing", NotificationType.Error );
Penumbra.Chat.NotificationMessage($"Could not load {LoadedShpkPath.ToPath()}:\n{e}", "Penumbra Advanced Editing",
NotificationType.Error);
}
if (LoadedShpkPath.InternalName.IsEmpty)
{
AssociatedShpkDevkit = null;
LoadedShpkDevkitPathName = string.Empty;
}
else
{
AssociatedShpkDevkit =
TryLoadShpkDevkit(Path.GetFileNameWithoutExtension(Mtrl.ShaderPackage.Name), out LoadedShpkDevkitPathName);
}
UpdateShaderKeys();
Update();
}
public void UpdateTextureLabels()
private JObject? TryLoadShpkDevkit(string shpkBaseName, out string devkitPathName)
{
var samplers = Mtrl.GetSamplersByTexture( AssociatedShpk );
TextureLabels.Clear();
TextureLabelWidth = 50f * UiHelpers.Scale;
using( var _ = ImRaii.PushFont( UiBuilder.MonoFont ) )
try
{
for( var i = 0; i < Mtrl.Textures.Length; ++i )
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 (sampler, shpkSampler) = samplers[ i ];
var name = shpkSampler.HasValue ? shpkSampler.Value.Name : sampler.HasValue ? $"0x{sampler.Value.SamplerId:X8}" : $"#{i}";
TextureLabels.Add( name );
TextureLabelWidth = Math.Max( TextureLabelWidth, ImGui.CalcTextSize( name ).X );
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.ColorSets.Any(c => c.HasRows))
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.ColorSets.Any(c => c.HasRows))
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.FindOrAddColorSet();
}
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;
}
public void UpdateShaderKeyLabels()
private void UpdateConstants()
{
ShaderKeyLabels.Clear();
DefinedShaderKeys.Clear();
foreach( var (key, idx) in Mtrl.ShaderPackage.ShaderKeys.WithIndex() )
static List<T> FindOrAddGroup<T>(List<(string, List<T>)> groups, string name)
{
ShaderKeyLabels.Add( $"#{idx}: 0x{key.Category:X8} = 0x{key.Value:X8}###{idx}: 0x{key.Category:X8}" );
DefinedShaderKeys.Add( key.Category, key.Value );
}
MissingShaderKeyIndices.Clear();
AvailableKeyValues.Clear();
var vertexShaders = new IndexSet( AssociatedShpk?.VertexShaders.Length ?? 0, false );
var pixelShaders = new IndexSet( AssociatedShpk?.PixelShaders.Length ?? 0, false );
if( AssociatedShpk != null )
{
MissingShaderKeyIndices.AddRange( AssociatedShpk.MaterialKeys.WithIndex().Where( k => !DefinedShaderKeys.ContainsKey( k.Value.Id ) ).WithoutValue() );
if( MissingShaderKeyIndices.Count > 0 && MissingShaderKeyIndices.All( i => AssociatedShpk.MaterialKeys[ i ].Id != NewKeyId ) )
foreach (var (groupName, group) in groups)
{
var key = AssociatedShpk.MaterialKeys[ MissingShaderKeyIndices[ 0 ] ];
NewKeyId = key.Id;
NewKeyDefault = key.DefaultValue;
if (string.Equals(name, groupName, StringComparison.Ordinal))
return group;
}
AvailableKeyValues.AddRange( AssociatedShpk.MaterialKeys.Select( k => DefinedShaderKeys.TryGetValue( k.Id, out var value ) ? value : k.DefaultValue ) );
foreach( var node in AssociatedShpk.Nodes )
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())
{
if( node.MaterialKeys.WithIndex().All( key => key.Value == AvailableKeyValues[ key.Index ] ) )
var values = Mtrl.GetConstantValues(constant);
for (var i = 0; i < values.Length; i += 4)
{
foreach( var pass in node.Passes )
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)
{
vertexShaders.Add( ( int )pass.VertexShader );
pixelShaders.Add( ( int )pass.PixelShader );
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(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));
}
}
}
}
}
VertexShaders = $"Vertex Shaders: {( vertexShaders.Count > 0 ? string.Join( ", ", vertexShaders.Select( i => $"#{i}" ) ) : "???" )}";
PixelShaders = $"Pixel Shaders: {( pixelShaders.Count > 0 ? string.Join( ", ", pixelShaders.Select( i => $"#{i}" ) ) : "???" )}";
}
public void UpdateConstantLabels()
{
var prefix = AssociatedShpk?.GetConstantById( MaterialParamsConstantId )?.Name ?? string.Empty;
MaterialConstantLabel = prefix.Length == 0 ? "Constants###Constants" : prefix + "###Constants";
DefinedMaterialConstants.Clear();
MaterialConstants.Clear();
HasMalformedMaterialConstants = false;
AliasedMaterialValueCount = 0;
OrphanedMaterialValues = new IndexSet( Mtrl.ShaderPackage.ShaderValues.Length, true );
foreach( var (constant, idx) in Mtrl.ShaderPackage.Constants.WithIndex() )
Constants.RemoveAll(group => group.Constants.Count == 0);
Constants.Sort((x, y) =>
{
DefinedMaterialConstants.Add( constant.Id );
var values = Mtrl.GetConstantValues( constant );
var paramValueOffset = -values.Length;
if( values.Length > 0 )
{
var shpkParam = AssociatedShpk?.GetMaterialParamById( constant.Id );
var paramByteOffset = shpkParam?.ByteOffset ?? -1;
if( ( paramByteOffset & 0x3 ) == 0 )
{
paramValueOffset = paramByteOffset >> 2;
}
if (string.Equals(x.Header, "Further Constants", StringComparison.Ordinal))
return 1;
if (string.Equals(y.Header, "Further Constants", StringComparison.Ordinal))
return -1;
var unique = OrphanedMaterialValues.RemoveRange( constant.ByteOffset >> 2, values.Length );
AliasedMaterialValueCount += values.Length - unique;
}
else
{
HasMalformedMaterialConstants = true;
}
var (name, componentOnly) = MaterialParamRangeName( prefix, paramValueOffset, values.Length );
var label = name == null
? $"#{idx:D2} (ID: 0x{constant.Id:X8})###{constant.Id}"
: $"#{idx:D2}: {name} (ID: 0x{constant.Id:X8})###{constant.Id}";
MaterialConstants.Add( ( label, componentOnly, paramValueOffset ) );
}
MissingMaterialConstants.Clear();
if( AssociatedShpk != null )
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)
{
var setIdx = false;
foreach( var param in AssociatedShpk.MaterialParams.Where( m => !DefinedMaterialConstants.Contains( m.Id ) ) )
{
var (name, _) = MaterialParamRangeName( prefix, param.ByteOffset >> 2, param.ByteSize >> 2 );
var label = name == null
? $"(ID: 0x{param.Id:X8})"
: $"{name} (ID: 0x{param.Id:X8})";
if( NewConstantId == param.Id )
{
setIdx = true;
NewConstantIdx = MissingMaterialConstants.Count;
}
MissingMaterialConstants.Add( ( label, param.Id, param.ByteSize ) );
}
if( !setIdx && MissingMaterialConstants.Count > 0 )
{
NewConstantIdx = 0;
NewConstantId = MissingMaterialConstants[ 0 ].Id;
}
group.Sort((x, y) => string.CompareOrdinal(
x.MonoFont ? x.Label.Replace("].w", "].{") : x.Label,
y.MonoFont ? y.Label.Replace("].w", "].{") : y.Label));
}
}
public void UpdateSamplers()
public unsafe void BindToMaterialInstances()
{
Samplers.Clear();
DefinedSamplers.Clear();
OrphanedSamplers = new IndexSet( Mtrl.Textures.Length, true );
foreach( var (sampler, idx) in Mtrl.ShaderPackage.Samplers.WithIndex() )
{
DefinedSamplers.Add( sampler.SamplerId );
if( !OrphanedSamplers.Remove( sampler.TextureIndex ) )
{
++AliasedSamplerCount;
}
UnbindFromMaterialInstances();
var shpk = AssociatedShpk?.GetSamplerById( sampler.SamplerId );
var label = shpk.HasValue
? $"#{idx}: {shpk.Value.Name} (ID: 0x{sampler.SamplerId:X8})##{sampler.SamplerId}"
: $"#{idx} (ID: 0x{sampler.SamplerId:X8})##{sampler.SamplerId}";
var fileName = $"Texture #{sampler.TextureIndex} - {Path.GetFileName( Mtrl.Textures[ sampler.TextureIndex ].Path )}";
Samplers.Add( ( label, fileName ) );
var instances = MaterialInfo.FindMaterials(_edit._dalamud.Objects, FilePath);
var foundMaterials = new HashSet<nint>();
foreach (var materialInfo in instances)
{
var drawObject = (CharacterBase*)MaterialInfo.GetDrawObject(materialInfo.Type, _edit._dalamud.Objects);
var material = materialInfo.GetDrawObjectMaterial(drawObject);
if (foundMaterials.Contains((nint)material))
continue;
try
{
MaterialPreviewers.Add(new LiveMaterialPreviewer(_edit._dalamud.Objects, materialInfo));
foundMaterials.Add((nint)material);
}
catch (InvalidOperationException)
{
// Carry on without that previewer.
}
}
MissingSamplers.Clear();
if( AssociatedShpk != null )
UpdateMaterialPreview();
var colorSet = Mtrl.ColorSets.FirstOrNull(colorSet => colorSet.HasRows);
if (!colorSet.HasValue)
return;
foreach (var materialInfo in instances)
{
var setSampler = false;
foreach( var sampler in AssociatedShpk.Samplers.Where( s => s.Slot == 2 && !DefinedSamplers.Contains( s.Id ) ) )
try
{
if( sampler.Id == NewSamplerId )
{
setSampler = true;
NewSamplerIdx = MissingSamplers.Count;
}
MissingSamplers.Add( ( sampler.Name, sampler.Id ) );
ColorSetPreviewers.Add(new LiveColorSetPreviewer(_edit._dalamud.Objects, _edit._dalamud.Framework, materialInfo));
}
if( !setSampler && MissingSamplers.Count > 0 )
catch (InvalidOperationException)
{
NewSamplerIdx = 0;
NewSamplerId = MissingSamplers[ 0 ].Id;
// Carry on without that previewer.
}
}
UpdateColorSetPreview();
}
private void UnbindFromMaterialInstances()
{
foreach (var previewer in MaterialPreviewers)
previewer.Dispose();
MaterialPreviewers.Clear();
foreach (var previewer in ColorSetPreviewers)
previewer.Dispose();
ColorSetPreviewers.Clear();
}
private unsafe void UnbindFromDrawObjectMaterialInstances(nint characterBase)
{
for (var i = MaterialPreviewers.Count; i-- > 0;)
{
var previewer = MaterialPreviewers[i];
if ((nint)previewer.DrawObject != characterBase)
continue;
previewer.Dispose();
MaterialPreviewers.RemoveAt(i);
}
for (var i = ColorSetPreviewers.Count; i-- > 0;)
{
var previewer = ColorSetPreviewers[i];
if ((nint)previewer.DrawObject != characterBase)
continue;
previewer.Dispose();
ColorSetPreviewers.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 HighlightColorSetRow(int rowIdx)
{
var oldRowIdx = HighlightedColorSetRow;
if (HighlightedColorSetRow != rowIdx)
{
HighlightedColorSetRow = rowIdx;
HighlightTime.Restart();
}
if (oldRowIdx >= 0)
UpdateColorSetRowPreview(oldRowIdx);
if (rowIdx >= 0)
UpdateColorSetRowPreview(rowIdx);
}
public void CancelColorSetHighlight()
{
var rowIdx = HighlightedColorSetRow;
HighlightedColorSetRow = -1;
HighlightTime.Reset();
if (rowIdx >= 0)
UpdateColorSetRowPreview(rowIdx);
}
public void UpdateColorSetRowPreview(int rowIdx)
{
if (ColorSetPreviewers.Count == 0)
return;
var maybeColorSet = Mtrl.ColorSets.FirstOrNull(colorSet => colorSet.HasRows);
if (!maybeColorSet.HasValue)
return;
var colorSet = maybeColorSet.Value;
var maybeColorDyeSet = Mtrl.ColorDyeSets.FirstOrNull(colorDyeSet => colorDyeSet.Index == colorSet.Index);
var row = colorSet.Rows[rowIdx];
if (maybeColorDyeSet.HasValue && UseColorDyeSet)
{
var stm = _edit._stainService.StmFile;
var dye = maybeColorDyeSet.Value.Rows[rowIdx];
if (stm.TryGetValue(dye.Template, _edit._stainService.StainCombo.CurrentSelection.Key, out var dyes))
row.ApplyDyeTemplate(dye, dyes);
}
if (HighlightedColorSetRow == rowIdx)
ApplyHighlight(ref row, (float)HighlightTime.Elapsed.TotalSeconds);
foreach (var previewer in ColorSetPreviewers)
{
row.AsHalves().CopyTo(previewer.ColorSet.AsSpan()
.Slice(LiveColorSetPreviewer.TextureWidth * 4 * rowIdx, LiveColorSetPreviewer.TextureWidth * 4));
previewer.ScheduleUpdate();
}
}
public void UpdateColorSetPreview()
{
if (ColorSetPreviewers.Count == 0)
return;
var maybeColorSet = Mtrl.ColorSets.FirstOrNull(colorSet => colorSet.HasRows);
if (!maybeColorSet.HasValue)
return;
var colorSet = maybeColorSet.Value;
var maybeColorDyeSet = Mtrl.ColorDyeSets.FirstOrNull(colorDyeSet => colorDyeSet.Index == colorSet.Index);
var rows = colorSet.Rows;
if (maybeColorDyeSet.HasValue && UseColorDyeSet)
{
var stm = _edit._stainService.StmFile;
var stainId = (StainId)_edit._stainService.StainCombo.CurrentSelection.Key;
var colorDyeSet = maybeColorDyeSet.Value;
for (var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i)
{
ref var row = ref rows[i];
var dye = colorDyeSet.Rows[i];
if (stm.TryGetValue(dye.Template, stainId, out var dyes))
row.ApplyDyeTemplate(dye, dyes);
}
}
if (HighlightedColorSetRow >= 0)
ApplyHighlight(ref rows[HighlightedColorSetRow], (float)HighlightTime.Elapsed.TotalSeconds);
foreach (var previewer in ColorSetPreviewers)
{
rows.AsHalves().CopyTo(previewer.ColorSet);
previewer.ScheduleUpdate();
}
}
private static void ApplyHighlight(ref MtrlFile.ColorSet.Row row, float time)
{
var level = Math.Sin(time * 2.0 * Math.PI) * 0.25 + 0.5;
var levelSq = (float)(level * level);
row.Diffuse = Vector3.Zero;
row.Specular = Vector3.Zero;
row.Emissive = new Vector3(levelSq);
}
public void Update()
{
UpdateTextureLabels();
UpdateShaderKeyLabels();
UpdateConstantLabels();
UpdateSamplers();
UpdateShaders();
UpdateTextures();
UpdateConstants();
}
public MtrlTab( ModEditWindow edit, MtrlFile file )
public MtrlTab(ModEditWindow edit, MtrlFile file, string filePath, bool writable)
{
_edit = edit;
Mtrl = file;
LoadShpk( FindAssociatedShpk( out _, out _ ) );
_edit = edit;
Mtrl = file;
FilePath = filePath;
Writable = writable;
UseColorDyeSet = file.ColorDyeSets.Length > 0;
AssociatedBaseDevkit = TryLoadShpkDevkit("_base", out LoadedBaseDevkitPathName);
LoadShpk(FindAssociatedShpk(out _, out _));
if (writable)
{
_edit._gameEvents.CharacterBaseDestructor += UnbindFromDrawObjectMaterialInstances;
BindToMaterialInstances();
}
}
public void Dispose()
{
UnbindFromMaterialInstances();
if (Writable)
_edit._gameEvents.CharacterBaseDestructor -= UnbindFromDrawObjectMaterialInstances;
}
public bool Valid
=> Mtrl.Valid;
=> ShadersKnown && Mtrl.Valid;
public byte[] Write()
=> Mtrl.Write();
{
var output = Mtrl.Clone();
output.GarbageCollect(AssociatedShpk, SamplerIds, UseColorDyeSet);
return output.Write();
}
private sealed record DevkitShaderKeyValue(string Label = "", string Description = "");
private sealed class DevkitShaderKey
{
public string Label = string.Empty;
public string Description = string.Empty;
public Dictionary<uint, DevkitShaderKeyValue> Values = new();
}
private sealed record DevkitSampler(string Label = "", string Description = "", string DefaultTexture = "");
private enum DevkitConstantType
{
Hidden = -1,
Float = 0,
Integer = 1,
Color = 2,
Enum = 3,
}
private sealed record DevkitConstantValue(string Label = "", string Description = "", float Value = 0);
private sealed class DevkitConstant
{
public uint Offset = 0;
public uint? Length = null;
public string Group = string.Empty;
public string Label = string.Empty;
public string Description = string.Empty;
public DevkitConstantType Type = DevkitConstantType.Float;
public float? Minimum = null;
public float? Maximum = null;
public float? Speed = null;
public float RelativeSpeed = 0.0f;
public float Factor = 1.0f;
public float Bias = 0.0f;
public byte Precision = 3;
public string Unit = string.Empty;
public bool SquaredRgb = false;
public bool Clamped = false;
public DevkitConstantValue[] Values = Array.Empty<DevkitConstantValue>();
public IConstantEditor? CreateEditor()
=> Type switch
{
DevkitConstantType.Hidden => null,
DevkitConstantType.Float => new FloatConstantEditor(Minimum, Maximum, Speed ?? 0.1f, RelativeSpeed, Factor, Bias, Precision,
Unit),
DevkitConstantType.Integer => new IntConstantEditor(ToInteger(Minimum), ToInteger(Maximum), Speed ?? 0.25f, RelativeSpeed,
Factor, Bias, Unit),
DevkitConstantType.Color => new ColorConstantEditor(SquaredRgb, Clamped),
DevkitConstantType.Enum => new EnumConstantEditor(Array.ConvertAll(Values,
value => (value.Label, value.Value, value.Description))),
_ => FloatConstantEditor.Default,
};
private static int? ToInteger(float? value)
=> value.HasValue ? (int)Math.Clamp(MathF.Round(value.Value), int.MinValue, int.MaxValue) : null;
}
}
}
}

View file

@ -1,16 +1,12 @@
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using System.Numerics;
using System.Text;
using Dalamud.Interface;
using Dalamud.Interface.ImGuiFileDialog;
using ImGuiNET;
using Lumina.Data.Parsing;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData;
using Penumbra.GameData.Files;
using Penumbra.String.Classes;
namespace Penumbra.UI.AdvancedWindow;
@ -19,37 +15,107 @@ public partial class ModEditWindow
{
private readonly FileDialogService _fileDialog;
private bool DrawPackageNameInput(MtrlTab tab, bool disabled)
// 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[]
{
var ret = false;
ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f);
if (ImGui.InputText("Shader Package Name", ref tab.Mtrl.ShaderPackage.Name, 63,
disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))
"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",
"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)
{
ret = true;
tab.AssociatedShpk = null;
tab.LoadedShpkPath = FullPath.Empty;
ImGui.TextUnformatted("Shader Package: " + tab.Mtrl.ShaderPackage.Name);
return false;
}
if (ImGui.IsItemDeactivatedAfterEdit())
tab.LoadShpk(tab.FindAssociatedShpk(out _, out _));
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(MtrlFile file, bool disabled)
private static bool DrawShaderFlagsInput(MtrlTab tab, bool disabled)
{
var ret = false;
var shpkFlags = (int)file.ShaderPackage.Flags;
ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f);
if (ImGui.InputInt("Shader Package Flags", ref shpkFlags, 0, 0,
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)))
{
file.ShaderPackage.Flags = (uint)shpkFlags;
ret = true;
}
return false;
return ret;
tab.Mtrl.ShaderPackage.Flags = (uint)shpkFlags;
tab.SetShaderPackageFlags((uint)shpkFlags);
return true;
}
/// <summary>
@ -58,16 +124,22 @@ public partial class ModEditWindow
/// </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));
if (ImGui.Selectable(text))
ImGui.SetClipboardText(tab.LoadedShpkPathName);
ImGuiUtil.HoverTooltip("Click to copy file path to clipboard.");
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) =>
@ -93,94 +165,56 @@ public partial class ModEditWindow
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
}
private static bool DrawShaderKey(MtrlTab tab, bool disabled, ref int idx)
{
var ret = false;
using var t2 = ImRaii.TreeNode(tab.ShaderKeyLabels[idx], disabled ? ImGuiTreeNodeFlags.Leaf : 0);
if (!t2 || disabled)
return ret;
var key = tab.Mtrl.ShaderPackage.ShaderKeys[idx];
var shpkKey = tab.AssociatedShpk?.GetMaterialKeyById(key.Category);
if (shpkKey.HasValue)
{
ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f);
using var c = ImRaii.Combo("Value", $"0x{key.Value:X8}");
if (c)
foreach (var value in shpkKey.Value.Values)
{
if (ImGui.Selectable($"0x{value:X8}", value == key.Value))
{
tab.Mtrl.ShaderPackage.ShaderKeys[idx].Value = value;
ret = true;
tab.UpdateShaderKeyLabels();
}
}
}
if (ImGui.Button("Remove Key"))
{
tab.Mtrl.ShaderPackage.ShaderKeys = tab.Mtrl.ShaderPackage.ShaderKeys.RemoveItems(idx--);
ret = true;
tab.UpdateShaderKeyLabels();
}
return ret;
}
private static bool DrawNewShaderKey(MtrlTab tab)
{
ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f);
var ret = false;
using (var c = ImRaii.Combo("##NewConstantId", $"ID: 0x{tab.NewKeyId:X8}"))
{
if (c)
foreach (var idx in tab.MissingShaderKeyIndices)
{
var key = tab.AssociatedShpk!.MaterialKeys[idx];
if (ImGui.Selectable($"ID: 0x{key.Id:X8}", key.Id == tab.NewKeyId))
{
tab.NewKeyDefault = key.DefaultValue;
tab.NewKeyId = key.Id;
ret = true;
tab.UpdateShaderKeyLabels();
}
}
}
ImGui.SameLine();
if (ImGui.Button("Add Key"))
{
tab.Mtrl.ShaderPackage.ShaderKeys = tab.Mtrl.ShaderPackage.ShaderKeys.AddItem(new ShaderKey
{
Category = tab.NewKeyId,
Value = tab.NewKeyDefault,
});
ret = true;
tab.UpdateShaderKeyLabels();
}
return ret;
}
private static bool DrawMaterialShaderKeys(MtrlTab tab, bool disabled)
{
if (tab.Mtrl.ShaderPackage.ShaderKeys.Length <= 0
&& (disabled || tab.AssociatedShpk == null || tab.AssociatedShpk.MaterialKeys.Length <= 0))
return false;
using var t = ImRaii.TreeNode("Shader Keys");
if (!t)
if (tab.ShaderKeys.Count == 0)
return false;
var ret = false;
for (var idx = 0; idx < tab.Mtrl.ShaderPackage.ShaderKeys.Length; ++idx)
ret |= DrawShaderKey(tab, disabled, ref idx);
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 (!disabled && tab.AssociatedShpk != null && tab.MissingShaderKeyIndices.Count != 0)
ret |= DrawNewShaderKey(tab);
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;
}
@ -190,160 +224,65 @@ public partial class ModEditWindow
if (tab.AssociatedShpk == null)
return;
ImRaii.TreeNode(tab.VertexShaders, ImGuiTreeNodeFlags.Leaf).Dispose();
ImRaii.TreeNode(tab.PixelShaders, ImGuiTreeNodeFlags.Leaf).Dispose();
}
ImRaii.TreeNode(tab.VertexShadersString, ImGuiTreeNodeFlags.Leaf).Dispose();
ImRaii.TreeNode(tab.PixelShadersString, ImGuiTreeNodeFlags.Leaf).Dispose();
private static bool DrawMaterialConstantValues(MtrlTab tab, bool disabled, ref int idx)
{
var (name, componentOnly, paramValueOffset) = tab.MaterialConstants[idx];
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
using var t2 = ImRaii.TreeNode(name);
if (!t2)
return false;
font.Dispose();
var constant = tab.Mtrl.ShaderPackage.Constants[idx];
var ret = false;
var values = tab.Mtrl.GetConstantValues(constant);
if (values.Length > 0)
if (tab.ShaderComment.Length > 0)
{
var valueOffset = constant.ByteOffset >> 2;
for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx)
{
var paramName = MaterialParamName(componentOnly, paramValueOffset + valueIdx) ?? $"#{valueIdx}";
ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f);
if (ImGui.InputFloat($"{paramName} (at 0x{(valueOffset + valueIdx) << 2:X4})", ref values[valueIdx], 0.0f, 0.0f, "%.3f",
disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))
{
ret = true;
tab.UpdateConstantLabels();
}
}
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
ImGui.TextUnformatted(tab.ShaderComment);
}
else
{
ImRaii.TreeNode($"Offset: 0x{constant.ByteOffset:X4}", ImGuiTreeNodeFlags.Leaf).Dispose();
ImRaii.TreeNode($"Size: 0x{constant.ByteSize:X4}", ImGuiTreeNodeFlags.Leaf).Dispose();
}
if (!disabled
&& !tab.HasMalformedMaterialConstants
&& tab.OrphanedMaterialValues.Count == 0
&& tab.AliasedMaterialValueCount == 0
&& ImGui.Button("Remove Constant"))
{
tab.Mtrl.ShaderPackage.ShaderValues =
tab.Mtrl.ShaderPackage.ShaderValues.RemoveItems(constant.ByteOffset >> 2, constant.ByteSize >> 2);
tab.Mtrl.ShaderPackage.Constants = tab.Mtrl.ShaderPackage.Constants.RemoveItems(idx--);
for (var i = 0; i < tab.Mtrl.ShaderPackage.Constants.Length; ++i)
{
if (tab.Mtrl.ShaderPackage.Constants[i].ByteOffset >= constant.ByteOffset)
tab.Mtrl.ShaderPackage.Constants[i].ByteOffset -= constant.ByteSize;
}
ret = true;
tab.UpdateConstantLabels();
}
return ret;
}
private static bool DrawMaterialOrphans(MtrlTab tab, bool disabled)
{
using var t2 = ImRaii.TreeNode($"Orphan Values ({tab.OrphanedMaterialValues.Count})");
if (!t2)
return false;
var ret = false;
foreach (var idx in tab.OrphanedMaterialValues)
{
ImGui.SetNextItemWidth(ImGui.GetFontSize() * 10.0f);
if (ImGui.InputFloat($"#{idx} (at 0x{idx << 2:X4})",
ref tab.Mtrl.ShaderPackage.ShaderValues[idx], 0.0f, 0.0f, "%.3f",
disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))
{
ret = true;
tab.UpdateConstantLabels();
}
}
return ret;
}
private static bool DrawNewMaterialParam(MtrlTab tab)
{
ImGui.SetNextItemWidth(UiHelpers.Scale * 450.0f);
using (var font = ImRaii.PushFont(UiBuilder.MonoFont))
{
using var c = ImRaii.Combo("##NewConstantId", tab.MissingMaterialConstants[tab.NewConstantIdx].Name);
if (c)
foreach (var (constant, idx) in tab.MissingMaterialConstants.WithIndex())
{
if (ImGui.Selectable(constant.Name, constant.Id == tab.NewConstantId))
{
tab.NewConstantIdx = idx;
tab.NewConstantId = constant.Id;
}
}
}
ImGui.SameLine();
if (ImGui.Button("Add Constant"))
{
var (_, _, byteSize) = tab.MissingMaterialConstants[tab.NewConstantIdx];
tab.Mtrl.ShaderPackage.Constants = tab.Mtrl.ShaderPackage.Constants.AddItem(new MtrlFile.Constant
{
Id = tab.NewConstantId,
ByteOffset = (ushort)(tab.Mtrl.ShaderPackage.ShaderValues.Length << 2),
ByteSize = byteSize,
});
tab.Mtrl.ShaderPackage.ShaderValues = tab.Mtrl.ShaderPackage.ShaderValues.AddItem(0.0f, byteSize >> 2);
tab.UpdateConstantLabels();
return true;
}
return false;
}
private static bool DrawMaterialConstants(MtrlTab tab, bool disabled)
{
if (tab.Mtrl.ShaderPackage.Constants.Length == 0
&& tab.Mtrl.ShaderPackage.ShaderValues.Length == 0
&& (disabled || tab.AssociatedShpk == null || tab.AssociatedShpk.MaterialParams.Length == 0))
if (tab.Constants.Count == 0)
return false;
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
using var t = ImRaii.TreeNode(tab.MaterialConstantLabel);
if (!t)
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
if (!ImGui.CollapsingHeader("Material Constants"))
return false;
font.Dispose();
using var _ = ImRaii.PushId("MaterialConstants");
var ret = false;
for (var idx = 0; idx < tab.Mtrl.ShaderPackage.Constants.Length; ++idx)
ret |= DrawMaterialConstantValues(tab, disabled, ref idx);
foreach (var (header, group) in tab.Constants)
{
using var t = ImRaii.TreeNode(header, ImGuiTreeNodeFlags.DefaultOpen);
if (!t)
continue;
if (tab.OrphanedMaterialValues.Count > 0)
ret |= DrawMaterialOrphans(tab, disabled);
else if (!disabled && !tab.HasMalformedMaterialConstants && tab.MissingMaterialConstants.Count > 0)
ret |= DrawNewMaterialParam(tab);
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}");
if (editor.Draw(buffer[slice], disabled, 250.0f))
{
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, ref int idx)
private static bool DrawMaterialSampler(MtrlTab tab, bool disabled, int textureIdx, int samplerIdx)
{
var (label, filename) = tab.Samplers[idx];
using var tree = ImRaii.TreeNode(label);
if (!tree)
return false;
ImRaii.TreeNode(filename, ImGuiTreeNodeFlags.Leaf).Dispose();
var ret = false;
var sampler = tab.Mtrl.ShaderPackage.Samplers[idx];
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)
@ -354,125 +293,134 @@ public partial class ModEditWindow
}
}
ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f);
if (InputHexUInt16("Texture Flags", ref tab.Mtrl.Textures[sampler.TextureIndex].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 * 150.0f);
ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f);
if (ImGui.InputInt("Sampler Flags", ref samplerFlags, 0, 0,
ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)))
{
tab.Mtrl.ShaderPackage.Samplers[idx].Flags = (uint)samplerFlags;
ret = true;
}
if (!disabled
&& tab.OrphanedSamplers.Count == 0
&& tab.AliasedSamplerCount == 0
&& ImGui.Button("Remove Sampler"))
{
tab.Mtrl.Textures = tab.Mtrl.Textures.RemoveItems(sampler.TextureIndex);
tab.Mtrl.ShaderPackage.Samplers = tab.Mtrl.ShaderPackage.Samplers.RemoveItems(idx--);
for (var i = 0; i < tab.Mtrl.ShaderPackage.Samplers.Length; ++i)
{
if (tab.Mtrl.ShaderPackage.Samplers[i].TextureIndex >= sampler.TextureIndex)
--tab.Mtrl.ShaderPackage.Samplers[i].TextureIndex;
}
ret = true;
tab.UpdateSamplers();
tab.UpdateTextureLabels();
sampler.Flags = (uint)samplerFlags;
ret = true;
tab.SetSamplerFlags(sampler.SamplerId, (uint)samplerFlags);
}
return ret;
}
private static bool DrawMaterialNewSampler(MtrlTab tab)
{
var (name, id) = tab.MissingSamplers[tab.NewSamplerIdx];
ImGui.SetNextItemWidth(UiHelpers.Scale * 450.0f);
using (var c = ImRaii.Combo("##NewSamplerId", $"{name} (ID: 0x{id:X8})"))
{
if (c)
foreach (var (sampler, idx) in tab.MissingSamplers.WithIndex())
{
if (ImGui.Selectable($"{sampler.Name} (ID: 0x{sampler.Id:X8})", sampler.Id == tab.NewSamplerId))
{
tab.NewSamplerIdx = idx;
tab.NewSamplerId = sampler.Id;
}
}
}
ImGui.SameLine();
if (!ImGui.Button("Add Sampler"))
return false;
tab.Mtrl.ShaderPackage.Samplers = tab.Mtrl.ShaderPackage.Samplers.AddItem(new Sampler
{
SamplerId = tab.NewSamplerId,
TextureIndex = (byte)tab.Mtrl.Textures.Length,
Flags = 0,
});
tab.Mtrl.Textures = tab.Mtrl.Textures.AddItem(new MtrlFile.Texture
{
Path = string.Empty,
Flags = 0,
});
tab.UpdateSamplers();
tab.UpdateTextureLabels();
return true;
}
private static bool DrawMaterialSamplers(MtrlTab tab, bool disabled)
{
if (tab.Mtrl.ShaderPackage.Samplers.Length == 0
&& tab.Mtrl.Textures.Length == 0
&& (disabled || (tab.AssociatedShpk?.Samplers.All(sampler => sampler.Slot != 2) ?? false)))
return false;
using var t = ImRaii.TreeNode("Samplers");
if (!t)
return false;
var ret = false;
for (var idx = 0; idx < tab.Mtrl.ShaderPackage.Samplers.Length; ++idx)
ret |= DrawMaterialSampler(tab, disabled, ref idx);
if (tab.OrphanedSamplers.Count > 0)
{
using var t2 = ImRaii.TreeNode($"Orphan Textures ({tab.OrphanedSamplers.Count})");
if (t2)
foreach (var idx in tab.OrphanedSamplers)
{
ImRaii.TreeNode($"#{idx}: {Path.GetFileName(tab.Mtrl.Textures[idx].Path)} - {tab.Mtrl.Textures[idx].Flags:X4}",
ImGuiTreeNodeFlags.Leaf)
.Dispose();
}
}
else if (!disabled && tab.MissingSamplers.Count > 0 && tab.AliasedSamplerCount == 0 && tab.Mtrl.Textures.Length < 255)
{
ret |= DrawMaterialNewSampler(tab);
}
return ret;
}
private bool DrawMaterialShaderResources(MtrlTab tab, bool disabled)
private bool DrawMaterialShader(MtrlTab tab, bool disabled)
{
var ret = false;
if (!ImGui.CollapsingHeader("Advanced Shader Resources"))
return ret;
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.");
}
ret |= DrawPackageNameInput(tab, disabled);
ret |= DrawShaderFlagsInput(tab.Mtrl, disabled);
DrawCustomAssociations(tab);
ret |= DrawMaterialShaderKeys(tab, disabled);
DrawMaterialShaders(tab);
ret |= DrawMaterialConstants(tab, disabled);
ret |= DrawMaterialSamplers(tab, disabled);
return ret;
}
@ -495,25 +443,25 @@ public partial class ModEditWindow
};
}
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)
{
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,
};
if (valueLength == 0 || valueOffset < 0)
return (null, false);

View file

@ -6,189 +6,221 @@ using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Files;
using Penumbra.String.Classes;
using Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow
{
private readonly FileEditor< MtrlTab > _materialTab;
private readonly FileEditor<MtrlTab> _materialTab;
private bool DrawMaterialPanel( MtrlTab tab, bool disabled )
private bool DrawMaterialPanel(MtrlTab tab, bool disabled)
{
var ret = DrawMaterialTextureChange( tab, disabled );
DrawMaterialLivePreviewRebind(tab, disabled);
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
ret |= DrawBackFaceAndTransparency( tab.Mtrl, disabled );
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
var ret = DrawBackFaceAndTransparency(tab, disabled);
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
ret |= DrawMaterialColorSetChange( tab.Mtrl, disabled );
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
ret |= DrawMaterialShader(tab, disabled);
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
ret |= DrawMaterialShaderResources( tab, disabled );
ret |= DrawMaterialTextureChange(tab, disabled);
ret |= DrawMaterialColorSetChange(tab, disabled);
ret |= DrawMaterialConstants(tab, disabled);
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
DrawOtherMaterialDetails( tab.Mtrl, disabled );
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
DrawOtherMaterialDetails(tab.Mtrl, disabled);
return !disabled && ret;
}
private static bool DrawMaterialTextureChange( MtrlTab tab, bool disabled )
private static void DrawMaterialLivePreviewRebind(MtrlTab tab, bool disabled)
{
var ret = false;
using var table = ImRaii.Table( "##Textures", 2 );
ImGui.TableSetupColumn( "Path", ImGuiTableColumnFlags.WidthStretch );
ImGui.TableSetupColumn( "Name", ImGuiTableColumnFlags.WidthFixed, tab.TextureLabelWidth * UiHelpers.Scale );
for( var i = 0; i < tab.Mtrl.Textures.Length; ++i )
if (disabled)
return;
if (ImGui.Button("Reload live preview"))
tab.BindToMaterialInstances();
if (tab.MaterialPreviewers.Count != 0 || tab.ColorSetPreviewers.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( i );
var tmp = tab.Mtrl.Textures[ i ].Path;
using var _ = ImRaii.PushId(samplerI);
var tmp = tab.Mtrl.Textures[textureI].Path;
var unfolded = tab.UnfoldedTextures.Contains(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[ i ].Path )
if (ImGuiUtil.DrawDisabledButton((unfolded ? FontAwesomeIcon.CaretDown : FontAwesomeIcon.CaretRight).ToIconString(),
new Vector2(frameHeight),
"Settings for this texture and the associated sampler", false, true))
{
ret = true;
tab.Mtrl.Textures[ i ].Path = tmp;
unfolded = !unfolded;
if (unfolded)
tab.UnfoldedTextures.Add(samplerI);
else
tab.UnfoldedTextures.Remove(samplerI);
}
ImGui.TableNextColumn();
using var font = ImRaii.PushFont( UiBuilder.MonoFont );
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted( tab.TextureLabels[ i ] );
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 (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(tab, disabled, textureI, samplerI);
ImGui.TableNextColumn();
}
}
return ret;
}
private static bool DrawBackFaceAndTransparency( MtrlFile file, bool disabled )
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 );
using var dis = ImRaii.Disabled(disabled);
var tmp = ( file.ShaderPackage.Flags & transparencyBit ) != 0;
if( ImGui.Checkbox( "Enable Transparency", ref tmp ) )
var tmp = (tab.Mtrl.ShaderPackage.Flags & transparencyBit) != 0;
if (ImGui.Checkbox("Enable Transparency", ref tmp))
{
file.ShaderPackage.Flags = tmp ? file.ShaderPackage.Flags | transparencyBit : file.ShaderPackage.Flags & ~transparencyBit;
ret = true;
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 = ( file.ShaderPackage.Flags & backfaceBit ) != 0;
if( ImGui.Checkbox( "Hide Backfaces", ref tmp ) )
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))
{
file.ShaderPackage.Flags = tmp ? file.ShaderPackage.Flags | backfaceBit : file.ShaderPackage.Flags & ~backfaceBit;
ret = true;
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 _ )
private static void DrawOtherMaterialDetails(MtrlFile file, bool _)
{
if( !ImGui.CollapsingHeader( "Further Content" ) )
{
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( "UV Sets", ImGuiTreeNodeFlags.DefaultOpen ) )
{
if( sets )
{
foreach( var set in file.UvSets )
{
ImRaii.TreeNode( $"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf ).Dispose();
}
}
}
if( file.AdditionalData.Length <= 0 )
{
if (file.AdditionalData.Length <= 0)
return;
}
using var t = ImRaii.TreeNode( $"Additional Data (Size: {file.AdditionalData.Length})###AdditionalData" );
if( t )
{
ImGuiUtil.TextWrapped( string.Join( ' ', file.AdditionalData.Select( c => $"{c:X2}" ) ) );
}
using var t = ImRaii.TreeNode($"Additional Data (Size: {file.AdditionalData.Length})###AdditionalData");
if (t)
ImGuiUtil.TextWrapped(string.Join(' ', file.AdditionalData.Select(c => $"{c:X2}")));
}
private void DrawMaterialReassignmentTab()
{
if( _editor.Files.Mdl.Count == 0 )
{
if (_editor.Files.Mdl.Count == 0)
return;
}
using var tab = ImRaii.TabItem( "Material Reassignment" );
if( !tab )
{
using var tab = ImRaii.TabItem("Material Reassignment");
if (!tab)
return;
}
ImGui.NewLine();
MaterialSuffix.Draw( _editor, ImGuiHelpers.ScaledVector2( 175, 0 ) );
MaterialSuffix.Draw(_editor, ImGuiHelpers.ScaledVector2(175, 0));
ImGui.NewLine();
using var child = ImRaii.Child( "##mdlFiles", -Vector2.One, true );
if( !child )
{
using var child = ImRaii.Child("##mdlFiles", -Vector2.One, true);
if (!child)
return;
}
using var table = ImRaii.Table( "##files", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.One );
if( !table )
{
using var table = ImRaii.Table("##files", 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() )
foreach (var (info, idx) in _editor.MdlMaterialEditor.ModelFiles.WithIndex())
{
using var id = ImRaii.PushId( idx );
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 (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Save.ToIconString(), iconSize,
"Save the changed mdl file.\nUse at own risk!", !info.Changed, true))
info.Save();
}
ImGui.TableNextColumn();
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Recycle.ToIconString(), iconSize,
"Restore current changes to default.", !info.Changed, true ) )
{
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Recycle.ToIconString(), iconSize,
"Restore current changes to default.", !info.Changed, true))
info.Restore();
}
ImGui.TableNextColumn();
ImGui.TextUnformatted( info.Path.FullName[ ( _mod!.ModPath.FullName.Length + 1 ).. ] );
ImGui.TextUnformatted(info.Path.FullName[(_mod!.ModPath.FullName.Length + 1)..]);
ImGui.TableNextColumn();
ImGui.SetNextItemWidth( 400 * UiHelpers.Scale );
var tmp = info.CurrentMaterials[ 0 ];
if( ImGui.InputText( "##0", ref tmp, 64 ) )
{
info.SetMaterial( tmp, 0 );
}
ImGui.SetNextItemWidth(400 * UiHelpers.Scale);
var tmp = info.CurrentMaterials[0];
if (ImGui.InputText("##0", ref tmp, 64))
info.SetMaterial(tmp, 0);
for( var i = 1; i < info.Count; ++i )
for (var i = 1; i < info.Count; ++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 ) )
{
info.SetMaterial( tmp, i );
}
ImGui.SetNextItemWidth(400 * UiHelpers.Scale);
tmp = info.CurrentMaterials[i];
if (ImGui.InputText($"##{i}", ref tmp, 64))
info.SetMaterial(tmp, i);
}
}
}
}
}

View file

@ -5,7 +5,6 @@ using Penumbra.GameData.Files;
using Penumbra.String.Classes;
using System.Globalization;
using System.Linq;
using Penumbra.UI.AdvancedWindow;
namespace Penumbra.UI.AdvancedWindow;

View file

@ -14,33 +14,38 @@ using Penumbra.GameData;
using Penumbra.GameData.Data;
using Penumbra.GameData.Files;
using Penumbra.String;
using Penumbra.UI.AdvancedWindow;
using static Penumbra.GameData.Files.ShpkFile;
namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow
{
private static readonly ByteString DisassemblyLabel = ByteString.FromSpanUnsafe( "##disassembly"u8, true, true, true );
private static readonly ByteString DisassemblyLabel = ByteString.FromSpanUnsafe("##disassembly"u8, true, true, true);
private readonly FileEditor< ShpkTab > _shaderPackageTab;
private readonly FileEditor<ShpkTab> _shaderPackageTab;
private static bool DrawShaderPackagePanel( ShpkTab file, bool disabled )
private static bool DrawShaderPackagePanel(ShpkTab file, bool disabled)
{
DrawShaderPackageSummary( file );
DrawShaderPackageSummary(file);
var ret = false;
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
ret |= DrawShaderPackageShaderArray( file, "Vertex Shader", file.Shpk.VertexShaders, disabled );
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
ret |= DrawShaderPackageShaderArray(file, "Vertex Shader", file.Shpk.VertexShaders, disabled);
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
ret |= DrawShaderPackageShaderArray( file, "Pixel Shader", file.Shpk.PixelShaders, disabled );
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
ret |= DrawShaderPackageShaderArray(file, "Pixel Shader", file.Shpk.PixelShaders, disabled);
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
ret |= DrawShaderPackageMaterialParamLayout( file, disabled );
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
ret |= DrawShaderPackageMaterialParamLayout(file, disabled);
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
ret |= DrawOtherShaderPackageDetails( file, disabled );
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
ret |= DrawShaderPackageResources(file, disabled);
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
DrawShaderPackageSelection(file);
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
DrawOtherShaderPackageDetails(file);
file.FileDialog.Draw();
@ -49,17 +54,26 @@ public partial class ModEditWindow
return !disabled && ret;
}
private static void DrawShaderPackageSummary( ShpkTab tab )
=> ImGui.TextUnformatted( tab.Header );
private static void DrawShaderExportButton( ShpkTab tab, string objectName, Shader shader, int idx )
private static void DrawShaderPackageSummary(ShpkTab tab)
{
if( !ImGui.Button( $"Export Shader Program Blob ({shader.Blob.Length} bytes)" ) )
ImGui.TextUnformatted(tab.Header);
if (!tab.Shpk.Disassembled)
{
return;
}
var textColor = ImGui.GetColorU32(ImGuiCol.Text);
var textColorWarning = (textColor & 0xFF000000u) | ((textColor & 0x00FEFEFE) >> 1) | 0x80u; // Half red
var defaultName = objectName[ 0 ] switch
using var c = ImRaii.PushColor(ImGuiCol.Text, textColorWarning);
ImGui.TextUnformatted("Your system doesn't support disassembling shaders. Some functionality will be missing.");
}
}
private static void DrawShaderExportButton(ShpkTab tab, string objectName, Shader shader, int idx)
{
if (!ImGui.Button($"Export Shader Program Blob ({shader.Blob.Length} bytes)"))
return;
var defaultName = objectName[0] switch
{
'V' => $"vs{idx}",
'P' => $"ps{idx}",
@ -67,244 +81,225 @@ public partial class ModEditWindow
};
var blob = shader.Blob;
tab.FileDialog.OpenSavePicker( $"Export {objectName} #{idx} Program Blob to...", tab.Extension, defaultName, tab.Extension, ( success, name ) =>
{
if( !success )
tab.FileDialog.OpenSavePicker($"Export {objectName} #{idx} Program Blob to...", tab.Extension, defaultName, tab.Extension,
(success, name) =>
{
return;
}
if (!success)
return;
try
{
File.WriteAllBytes( name, blob );
}
catch( Exception e )
{
Penumbra.Chat.NotificationMessage( $"Could not export {defaultName}{tab.Extension} to {name}:\n{e.Message}", "Penumbra Advanced Editing",
NotificationType.Error );
return;
}
try
{
File.WriteAllBytes(name, blob);
}
catch (Exception e)
{
Penumbra.Chat.NotificationMessage($"Could not export {defaultName}{tab.Extension} to {name}:\n{e.Message}",
"Penumbra Advanced Editing",
NotificationType.Error);
return;
}
Penumbra.Chat.NotificationMessage( $"Shader Program Blob {defaultName}{tab.Extension} exported successfully to {Path.GetFileName( name )}",
"Penumbra Advanced Editing", NotificationType.Success );
}, null, false );
Penumbra.Chat.NotificationMessage(
$"Shader Program Blob {defaultName}{tab.Extension} exported successfully to {Path.GetFileName(name)}",
"Penumbra Advanced Editing", NotificationType.Success);
}, null, false);
}
private static void DrawShaderImportButton( ShpkTab tab, string objectName, Shader[] shaders, int idx )
private static void DrawShaderImportButton(ShpkTab tab, string objectName, Shader[] shaders, int idx)
{
if( !ImGui.Button( "Replace Shader Program Blob" ) )
{
if (!ImGui.Button("Replace Shader Program Blob"))
return;
}
tab.FileDialog.OpenFilePicker( $"Replace {objectName} #{idx} Program Blob...", "Shader Program Blobs{.o,.cso,.dxbc,.dxil}", ( success, name ) =>
{
if( !success )
tab.FileDialog.OpenFilePicker($"Replace {objectName} #{idx} Program Blob...", "Shader Program Blobs{.o,.cso,.dxbc,.dxil}",
(success, name) =>
{
return;
}
if (!success)
return;
try
{
shaders[ idx ].Blob = File.ReadAllBytes(name[0] );
}
catch( Exception e )
{
Penumbra.Chat.NotificationMessage( $"Could not import {name}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error );
return;
}
try
{
shaders[idx].Blob = File.ReadAllBytes(name[0]);
}
catch (Exception e)
{
Penumbra.Chat.NotificationMessage($"Could not import {name}:\n{e.Message}", "Penumbra Advanced Editing",
NotificationType.Error);
return;
}
try
{
shaders[ idx ].UpdateResources( tab.Shpk );
tab.Shpk.UpdateResources();
}
catch( Exception e )
{
tab.Shpk.SetInvalid();
Penumbra.Chat.NotificationMessage( $"Failed to update resources after importing {name}:\n{e.Message}", "Penumbra Advanced Editing",
NotificationType.Error );
return;
}
try
{
shaders[idx].UpdateResources(tab.Shpk);
tab.Shpk.UpdateResources();
}
catch (Exception e)
{
tab.Shpk.SetInvalid();
Penumbra.Chat.NotificationMessage($"Failed to update resources after importing {name}:\n{e.Message}",
"Penumbra Advanced Editing",
NotificationType.Error);
return;
}
tab.Shpk.SetChanged();
}, 1, null, false );
tab.Shpk.SetChanged();
}, 1, null, false);
}
private static unsafe void DrawRawDisassembly( Shader shader )
private static unsafe void DrawRawDisassembly(Shader shader)
{
using var t2 = ImRaii.TreeNode( "Raw Program Disassembly" );
if( !t2 )
{
using var t2 = ImRaii.TreeNode("Raw Program Disassembly");
if (!t2)
return;
}
using var font = ImRaii.PushFont( UiBuilder.MonoFont );
var size = new Vector2( ImGui.GetContentRegionAvail().X, ImGui.GetTextLineHeight() * 20 );
ImGuiNative.igInputTextMultiline( DisassemblyLabel.Path, shader.Disassembly!.RawDisassembly.Path, ( uint )shader.Disassembly!.RawDisassembly.Length + 1, size,
ImGuiInputTextFlags.ReadOnly, null, null );
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
var size = new Vector2(ImGui.GetContentRegionAvail().X, ImGui.GetTextLineHeight() * 20);
ImGuiNative.igInputTextMultiline(DisassemblyLabel.Path, shader.Disassembly!.RawDisassembly.Path,
(uint)shader.Disassembly!.RawDisassembly.Length + 1, size,
ImGuiInputTextFlags.ReadOnly, null, null);
}
private static bool DrawShaderPackageShaderArray( ShpkTab tab, string objectName, Shader[] shaders, bool disabled )
private static bool DrawShaderPackageShaderArray(ShpkTab tab, string objectName, Shader[] shaders, bool disabled)
{
if( shaders.Length == 0 || !ImGui.CollapsingHeader( $"{objectName}s" ) )
{
if (shaders.Length == 0 || !ImGui.CollapsingHeader($"{objectName}s"))
return false;
}
var ret = false;
for( var idx = 0; idx < shaders.Length; ++idx )
for (var idx = 0; idx < shaders.Length; ++idx)
{
var shader = shaders[ idx ];
using var t = ImRaii.TreeNode( $"{objectName} #{idx}" );
if( !t )
{
var shader = shaders[idx];
using var t = ImRaii.TreeNode($"{objectName} #{idx}");
if (!t)
continue;
}
DrawShaderExportButton( tab, objectName, shader, idx );
if( !disabled )
DrawShaderExportButton(tab, objectName, shader, idx);
if (!disabled && tab.Shpk.Disassembled)
{
ImGui.SameLine();
DrawShaderImportButton( tab, objectName, shaders, idx );
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, true);
ret |= DrawShaderPackageResourceArray("Samplers", "slot", false, shader.Samplers, true);
ret |= DrawShaderPackageResourceArray("Unordered Access Views", "slot", true, shader.Uavs, true);
if( shader.AdditionalHeader.Length > 0 )
if (shader.AdditionalHeader.Length > 0)
{
using var t2 = ImRaii.TreeNode( $"Additional Header (Size: {shader.AdditionalHeader.Length})###AdditionalHeader" );
if( t2 )
{
ImGuiUtil.TextWrapped( string.Join( ' ', shader.AdditionalHeader.Select( c => $"{c:X2}" ) ) );
}
using var t2 = ImRaii.TreeNode($"Additional Header (Size: {shader.AdditionalHeader.Length})###AdditionalHeader");
if (t2)
ImGuiUtil.TextWrapped(string.Join(' ', shader.AdditionalHeader.Select(c => $"{c:X2}")));
}
DrawRawDisassembly( shader );
if (tab.Shpk.Disassembled)
DrawRawDisassembly(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 disabled)
{
var ret = false;
if( !disabled )
if (!disabled)
{
ImGui.SetNextItemWidth( UiHelpers.Scale * 150.0f );
if( ImGuiUtil.InputUInt16( $"{char.ToUpper( slotLabel[ 0 ] )}{slotLabel[ 1.. ].ToLower()}", ref resource.Slot, ImGuiInputTextFlags.None ) )
{
ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f);
if (ImGuiUtil.InputUInt16($"{char.ToUpper(slotLabel[0])}{slotLabel[1..].ToLower()}", ref resource.Slot, ImGuiInputTextFlags.None))
ret = true;
}
}
if( resource.Used == null )
{
if (resource.Used == null)
return ret;
}
var usedString = UsedComponentString( withSize, resource );
if( usedString.Length > 0 )
{
ImRaii.TreeNode( $"Used: {usedString}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose();
}
var usedString = UsedComponentString(withSize, resource);
if (usedString.Length > 0)
ImRaii.TreeNode($"Used: {usedString}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose();
else
{
ImRaii.TreeNode( "Unused", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose();
}
ImRaii.TreeNode("Unused", 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 disabled)
{
if( resources.Length == 0 )
{
if (resources.Length == 0)
return false;
}
using var t = ImRaii.TreeNode( arrayName );
if( !t )
{
using var t = ImRaii.TreeNode(arrayName);
if (!t)
return false;
}
var ret = false;
for( var idx = 0; idx < resources.Length; ++idx )
for (var idx = 0; idx < resources.Length; ++idx)
{
ref var buf = ref resources[ idx ];
ref var buf = ref resources[idx];
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 );
+ (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();
if( t2 )
{
ret |= DrawShaderPackageResource( slotLabel, withSize, ref buf, disabled );
}
if (t2)
ret |= DrawShaderPackageResource(slotLabel, withSize, ref buf, disabled);
}
return ret;
}
private static bool DrawMaterialParamLayoutHeader( string label )
private static bool DrawMaterialParamLayoutHeader(string label)
{
using var font = ImRaii.PushFont( UiBuilder.MonoFont );
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
var pos = ImGui.GetCursorScreenPos()
+ new Vector2( ImGui.CalcTextSize( label ).X + 3 * ImGui.GetStyle().ItemInnerSpacing.X + ImGui.GetFrameHeight(), ImGui.GetStyle().FramePadding.Y );
+ new Vector2(ImGui.CalcTextSize(label).X + 3 * ImGui.GetStyle().ItemInnerSpacing.X + ImGui.GetFrameHeight(),
ImGui.GetStyle().FramePadding.Y);
var ret = ImGui.CollapsingHeader( label );
ImGui.GetWindowDrawList().AddText( UiBuilder.DefaultFont, UiBuilder.DefaultFont.FontSize, pos, ImGui.GetColorU32( ImGuiCol.Text ), "Layout" );
var ret = ImGui.CollapsingHeader(label);
ImGui.GetWindowDrawList()
.AddText(UiBuilder.DefaultFont, UiBuilder.DefaultFont.FontSize, pos, ImGui.GetColorU32(ImGuiCol.Text), "Layout");
return ret;
}
private static bool DrawMaterialParamLayoutBufferSize( ShpkFile file, Resource? materialParams )
private static bool DrawMaterialParamLayoutBufferSize(ShpkFile file, Resource? materialParams)
{
var isSizeWellDefined = ( file.MaterialParamsSize & 0xF ) == 0 && ( !materialParams.HasValue || file.MaterialParamsSize == materialParams.Value.Size << 4 );
if( isSizeWellDefined )
{
var isSizeWellDefined = (file.MaterialParamsSize & 0xF) == 0
&& (!materialParams.HasValue || file.MaterialParamsSize == materialParams.Value.Size << 4);
if (isSizeWellDefined)
return true;
}
ImGui.TextUnformatted( materialParams.HasValue
ImGui.TextUnformatted(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" );
: $"Buffer size mismatch: {file.MaterialParamsSize} bytes, not a multiple of 16");
return false;
}
private static bool DrawShaderPackageMaterialMatrix( ShpkTab tab, bool disabled )
private static bool DrawShaderPackageMaterialMatrix(ShpkTab tab, bool disabled)
{
ImGui.TextUnformatted( "Parameter positions (continuations are grayed out, unused values are red):" );
ImGui.TextUnformatted(tab.Shpk.Disassembled
? "Parameter positions (continuations are grayed out, unused values are red):"
: "Parameter positions (continuations are grayed out):");
using var table = ImRaii.Table( "##MaterialParamLayout", 5,
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg );
if( !table )
{
using var table = ImRaii.Table("##MaterialParamLayout", 5,
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
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, 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.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 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 ret = false;
for( var i = 0; i < tab.Matrix.GetLength( 0 ); ++i )
for (var i = 0; i < tab.Matrix.GetLength(0); ++i)
{
ImGui.TableNextColumn();
ImGui.TableHeader( $" [{i}]" );
for( var j = 0; j < 4; ++j )
ImGui.TableHeader($" [{i}]");
for (var j = 0; j < 4; ++j)
{
var (name, tooltip, idx, colorType) = tab.Matrix[ i, j ];
var (name, tooltip, idx, colorType) = tab.Matrix[i, j];
var color = colorType switch
{
ShpkTab.ColorType.Unused => textColorUnusedStart,
@ -313,354 +308,307 @@ public partial class ModEditWindow
ShpkTab.ColorType.Continuation | ShpkTab.ColorType.Used => textColorCont,
_ => textColorStart,
};
using var _ = ImRaii.PushId( i * 4 + j );
using var _ = ImRaii.PushId(i * 4 + j);
var deletable = !disabled && idx >= 0;
using( var font = ImRaii.PushFont( UiBuilder.MonoFont, tooltip.Length > 0 ) )
using (var font = ImRaii.PushFont(UiBuilder.MonoFont, tooltip.Length > 0))
{
using( var c = ImRaii.PushColor( ImGuiCol.Text, color ) )
using (var c = ImRaii.PushColor(ImGuiCol.Text, color))
{
ImGui.TableNextColumn();
ImGui.Selectable( name );
if( deletable && ImGui.IsItemClicked( ImGuiMouseButton.Right ) && ImGui.GetIO().KeyCtrl )
ImGui.Selectable(name);
if (deletable && ImGui.IsItemClicked(ImGuiMouseButton.Right) && ImGui.GetIO().KeyCtrl)
{
tab.Shpk.MaterialParams = tab.Shpk.MaterialParams.RemoveItems( idx );
tab.Shpk.MaterialParams = tab.Shpk.MaterialParams.RemoveItems(idx);
ret = true;
tab.Update();
}
}
ImGuiUtil.HoverTooltip( tooltip );
ImGuiUtil.HoverTooltip(tooltip);
}
if( deletable )
{
ImGuiUtil.HoverTooltip( "\nControl + Right-Click to remove." );
}
if (deletable)
ImGuiUtil.HoverTooltip("\nControl + Right-Click to remove.");
}
}
return ret;
}
private static void DrawShaderPackageMisalignedParameters( ShpkTab tab )
private static void DrawShaderPackageMisalignedParameters(ShpkTab tab)
{
using var t = ImRaii.TreeNode( "Misaligned / Overflowing Parameters" );
if( !t )
{
using var t = ImRaii.TreeNode("Misaligned / Overflowing Parameters");
if (!t)
return;
}
using var _ = ImRaii.PushFont( UiBuilder.MonoFont );
foreach( var name in tab.MalformedParameters )
{
ImRaii.TreeNode( name, ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose();
}
using var _ = ImRaii.PushFont(UiBuilder.MonoFont);
foreach (var name in tab.MalformedParameters)
ImRaii.TreeNode(name, ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose();
}
private static void DrawShaderPackageStartCombo( ShpkTab tab )
private static void DrawShaderPackageStartCombo(ShpkTab tab)
{
using var s = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing );
using( var _ = ImRaii.PushFont( UiBuilder.MonoFont ) )
using var s = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing);
using (var _ = ImRaii.PushFont(UiBuilder.MonoFont))
{
ImGui.SetNextItemWidth( UiHelpers.Scale * 400 );
using var c = ImRaii.Combo( "##Start", tab.Orphans[ tab.NewMaterialParamStart ].Name );
if( c )
{
foreach( var (start, idx) in tab.Orphans.WithIndex() )
ImGui.SetNextItemWidth(UiHelpers.Scale * 400);
using var c = ImRaii.Combo("##Start", tab.Orphans[tab.NewMaterialParamStart].Name);
if (c)
foreach (var (start, idx) in tab.Orphans.WithIndex())
{
if( ImGui.Selectable( start.Name, idx == tab.NewMaterialParamStart ) )
{
tab.UpdateOrphanStart( idx );
}
if (ImGui.Selectable(start.Name, idx == tab.NewMaterialParamStart))
tab.UpdateOrphanStart(idx);
}
}
}
ImGui.SameLine();
ImGui.TextUnformatted( "Start" );
ImGui.TextUnformatted("Start");
}
private static void DrawShaderPackageEndCombo( ShpkTab tab )
private static void DrawShaderPackageEndCombo(ShpkTab tab)
{
using var s = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing );
using( var _ = ImRaii.PushFont( UiBuilder.MonoFont ) )
using var s = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing);
using (var _ = ImRaii.PushFont(UiBuilder.MonoFont))
{
ImGui.SetNextItemWidth( UiHelpers.Scale * 400 );
using var c = ImRaii.Combo( "##End", tab.Orphans[ tab.NewMaterialParamEnd ].Name );
if( c )
ImGui.SetNextItemWidth(UiHelpers.Scale * 400);
using var c = ImRaii.Combo("##End", tab.Orphans[tab.NewMaterialParamEnd].Name);
if (c)
{
var current = tab.Orphans[ tab.NewMaterialParamStart ].Index;
for( var i = tab.NewMaterialParamStart; i < tab.Orphans.Count; ++i )
var current = tab.Orphans[tab.NewMaterialParamStart].Index;
for (var i = tab.NewMaterialParamStart; i < tab.Orphans.Count; ++i)
{
var next = tab.Orphans[ i ];
if( current++ != next.Index )
{
var next = tab.Orphans[i];
if (current++ != next.Index)
break;
}
if( ImGui.Selectable( next.Name, i == tab.NewMaterialParamEnd ) )
{
if (ImGui.Selectable(next.Name, i == tab.NewMaterialParamEnd))
tab.NewMaterialParamEnd = i;
}
}
}
}
ImGui.SameLine();
ImGui.TextUnformatted( "End" );
ImGui.TextUnformatted("End");
}
private static bool DrawShaderPackageNewParameter( ShpkTab tab )
private static bool DrawShaderPackageNewParameter(ShpkTab tab)
{
if( tab.Orphans.Count == 0 )
{
if (tab.Orphans.Count == 0)
return false;
}
DrawShaderPackageStartCombo( tab );
DrawShaderPackageEndCombo( tab );
DrawShaderPackageStartCombo(tab);
DrawShaderPackageEndCombo(tab);
ImGui.SetNextItemWidth( UiHelpers.Scale * 400 );
if( ImGui.InputText( "Name", ref tab.NewMaterialParamName, 63 ) )
{
tab.NewMaterialParamId = Crc32.Get( tab.NewMaterialParamName, 0xFFFFFFFFu );
}
ImGui.SetNextItemWidth(UiHelpers.Scale * 400);
if (ImGui.InputText("Name", ref tab.NewMaterialParamName, 63))
tab.NewMaterialParamId = Crc32.Get(tab.NewMaterialParamName, 0xFFFFFFFFu);
var tooltip = tab.UsedIds.Contains( tab.NewMaterialParamId )
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 ) )
{
if (!ImGuiUtil.DrawDisabledButton($"Add ID 0x{tab.NewMaterialParamId:X8}", new Vector2(400 * UiHelpers.Scale, ImGui.GetFrameHeight()),
tooltip,
tooltip.Length > 0))
return false;
}
tab.Shpk.MaterialParams = tab.Shpk.MaterialParams.AddItem( new MaterialParam
tab.Shpk.MaterialParams = tab.Shpk.MaterialParams.AddItem(new MaterialParam
{
Id = tab.NewMaterialParamId,
ByteOffset = ( ushort )( tab.Orphans[ tab.NewMaterialParamStart ].Index << 2 ),
ByteSize = ( ushort )( ( tab.NewMaterialParamEnd - tab.NewMaterialParamStart + 1 ) << 2 ),
} );
ByteOffset = (ushort)(tab.Orphans[tab.NewMaterialParamStart].Index << 2),
ByteSize = (ushort)((tab.NewMaterialParamEnd - tab.NewMaterialParamStart + 1) << 2),
});
tab.Update();
return true;
}
private static bool DrawShaderPackageMaterialParamLayout( ShpkTab tab, bool disabled )
private static bool DrawShaderPackageMaterialParamLayout(ShpkTab tab, bool disabled)
{
var ret = false;
var materialParams = tab.Shpk.GetConstantById( MaterialParamsConstantId );
if( !DrawMaterialParamLayoutHeader( materialParams?.Name ?? "Material Parameter" ) )
{
var materialParams = tab.Shpk.GetConstantById(MaterialParamsConstantId);
if (!DrawMaterialParamLayoutHeader(materialParams?.Name ?? "Material Parameter"))
return false;
}
var sizeWellDefined = DrawMaterialParamLayoutBufferSize( tab.Shpk, materialParams );
var sizeWellDefined = DrawMaterialParamLayoutBufferSize(tab.Shpk, materialParams);
ret |= DrawShaderPackageMaterialMatrix( tab, disabled );
ret |= DrawShaderPackageMaterialMatrix(tab, disabled);
if( tab.MalformedParameters.Count > 0 )
{
DrawShaderPackageMisalignedParameters( tab );
}
else if( !disabled && sizeWellDefined )
{
ret |= DrawShaderPackageNewParameter( tab );
}
if (tab.MalformedParameters.Count > 0)
DrawShaderPackageMisalignedParameters(tab);
else if (!disabled && sizeWellDefined)
ret |= DrawShaderPackageNewParameter(tab);
return ret;
}
private static void DrawKeyArray( string arrayName, bool withId, IReadOnlyCollection< Key > keys )
private static bool DrawShaderPackageResources(ShpkTab tab, bool disabled)
{
if( keys.Count == 0 )
{
return;
}
var ret = false;
using var t = ImRaii.TreeNode( arrayName );
if( !t )
{
return;
}
if (!ImGui.CollapsingHeader("Shader Resources"))
return false;
using var font = ImRaii.PushFont( UiBuilder.MonoFont );
foreach( var (key, idx) in keys.WithIndex() )
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);
return ret;
}
private static void DrawKeyArray(string arrayName, bool withId, IReadOnlyCollection<Key> keys)
{
if (keys.Count == 0)
return;
using var t = ImRaii.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}" );
if( t2 )
using var t2 = ImRaii.TreeNode(withId ? $"#{idx}: 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}" ) )}",
ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose();
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}"))}",
ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose();
}
}
}
private static void DrawShaderPackageNodes( ShpkTab tab )
private static void DrawShaderPackageNodes(ShpkTab tab)
{
if( tab.Shpk.Nodes.Length <= 0 )
{
if (tab.Shpk.Nodes.Length <= 0)
return;
}
using var t = ImRaii.TreeNode( $"Nodes ({tab.Shpk.Nodes.Length})###Nodes" );
if( !t )
{
using var t = ImRaii.TreeNode($"Nodes ({tab.Shpk.Nodes.Length})###Nodes");
if (!t)
return;
}
foreach( var (node, idx) in tab.Shpk.Nodes.WithIndex() )
foreach (var (node, idx) in tab.Shpk.Nodes.WithIndex())
{
using var font = ImRaii.PushFont( UiBuilder.MonoFont );
using var t2 = ImRaii.TreeNode( $"#{idx:D4}: ID: 0x{node.Id:X8}" );
if( !t2 )
{
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
using var t2 = ImRaii.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}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose();
}
foreach (var (key, keyIdx) in node.SystemKeys.WithIndex())
ImRaii.TreeNode($"System Key 0x{tab.Shpk.SystemKeys[keyIdx].Id:X8} = 0x{key:X8}",
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}", 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}",
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}", 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}",
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();
}
foreach (var (key, keyIdx) in node.SubViewKeys.WithIndex())
ImRaii.TreeNode($"Sub-View Key #{keyIdx} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose();
ImRaii.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 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}",
ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet )
.Dispose();
ImRaii.TreeNode($"Pass #{passIdx}: ID: 0x{pass.Id:X8}, Vertex Shader #{pass.VertexShader}, Pixel Shader #{pass.PixelShader}",
ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet)
.Dispose();
}
}
}
private static bool DrawOtherShaderPackageDetails( ShpkTab tab, bool disabled )
private static void DrawShaderPackageSelection(ShpkTab tab)
{
var ret = false;
if (!ImGui.CollapsingHeader("Shader Selection"))
return;
if( !ImGui.CollapsingHeader( "Further Content" ) )
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);
DrawShaderPackageNodes(tab);
using var t = ImRaii.TreeNode($"Node Selectors ({tab.Shpk.NodeSelectors.Count})###NodeSelectors");
if (t)
{
return false;
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)
.Dispose();
}
ImRaii.TreeNode( $"Version: 0x{tab.Shpk.Version:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose();
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 );
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 );
DrawShaderPackageNodes( tab );
if( tab.Shpk.Items.Length > 0 )
{
using var t = ImRaii.TreeNode( $"Items ({tab.Shpk.Items.Length})###Items" );
if( t )
{
using var font = ImRaii.PushFont( UiBuilder.MonoFont );
foreach( var (item, idx) in tab.Shpk.Items.WithIndex() )
{
ImRaii.TreeNode( $"#{idx:D4}: ID: 0x{item.Id:X8}, node: {item.Node}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose();
}
}
}
if( tab.Shpk.AdditionalData.Length > 0 )
{
using var t = ImRaii.TreeNode( $"Additional Data (Size: {tab.Shpk.AdditionalData.Length})###AdditionalData" );
if( t )
{
ImGuiUtil.TextWrapped( string.Join( ' ', tab.Shpk.AdditionalData.Select( c => $"{c:X2}" ) ) );
}
}
return ret;
}
private static string UsedComponentString( bool withSize, in Resource resource )
private static void DrawOtherShaderPackageDetails(ShpkTab tab)
{
var sb = new StringBuilder( 256 );
if( withSize )
if (!ImGui.CollapsingHeader("Further Content"))
return;
ImRaii.TreeNode($"Version: 0x{tab.Shpk.Version:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose();
if (tab.Shpk.AdditionalData.Length > 0)
{
foreach( var (components, i) in ( resource.Used ?? Array.Empty< DisassembledShader.VectorComponents >() ).WithIndex() )
using var t = ImRaii.TreeNode($"Additional Data (Size: {tab.Shpk.AdditionalData.Length})###AdditionalData");
if (t)
ImGuiUtil.TextWrapped(string.Join(' ', tab.Shpk.AdditionalData.Select(c => $"{c:X2}")));
}
}
private static string UsedComponentString(bool withSize, in Resource resource)
{
var sb = new StringBuilder(256);
if (withSize)
{
foreach (var (components, i) in (resource.Used ?? Array.Empty<DisassembledShader.VectorComponents>()).WithIndex())
{
switch( components )
switch (components)
{
case 0: break;
case DisassembledShader.VectorComponents.All:
sb.Append( $"[{i}], " );
sb.Append($"[{i}], ");
break;
default:
sb.Append( $"[{i}]." );
foreach( var c in components.ToString().Where( char.IsUpper ) )
{
sb.Append( char.ToLower( c ) );
}
sb.Append($"[{i}].");
foreach (var c in components.ToString().Where(char.IsUpper))
sb.Append(char.ToLower(c));
sb.Append( ", " );
sb.Append(", ");
break;
}
}
switch( resource.UsedDynamically ?? 0 )
switch (resource.UsedDynamically ?? 0)
{
case 0: break;
case DisassembledShader.VectorComponents.All:
sb.Append( "[*], " );
sb.Append("[*], ");
break;
default:
sb.Append( "[*]." );
foreach( var c in resource.UsedDynamically!.Value.ToString().Where( char.IsUpper ) )
{
sb.Append( char.ToLower( c ) );
}
sb.Append("[*].");
foreach (var c in resource.UsedDynamically!.Value.ToString().Where(char.IsUpper))
sb.Append(char.ToLower(c));
sb.Append( ", " );
sb.Append(", ");
break;
}
}
else
{
var components = ( resource.Used is { Length: > 0 } ? resource.Used[ 0 ] : 0 ) | ( resource.UsedDynamically ?? 0 );
if( ( components & DisassembledShader.VectorComponents.X ) != 0 )
{
sb.Append( "Red, " );
}
var components = (resource.Used is { Length: > 0 } ? resource.Used[0] : 0) | (resource.UsedDynamically ?? 0);
if ((components & DisassembledShader.VectorComponents.X) != 0)
sb.Append("Red, ");
if( ( components & DisassembledShader.VectorComponents.Y ) != 0 )
{
sb.Append( "Green, " );
}
if ((components & DisassembledShader.VectorComponents.Y) != 0)
sb.Append("Green, ");
if( ( components & DisassembledShader.VectorComponents.Z ) != 0 )
{
sb.Append( "Blue, " );
}
if ((components & DisassembledShader.VectorComponents.Z) != 0)
sb.Append("Blue, ");
if( ( components & DisassembledShader.VectorComponents.W ) != 0 )
{
sb.Append( "Alpha, " );
}
if ((components & DisassembledShader.VectorComponents.W) != 0)
sb.Append("Alpha, ");
}
return sb.Length == 0 ? string.Empty : sb.ToString( 0, sb.Length - 2 );
return sb.Length == 0 ? string.Empty : sb.ToString(0, sb.Length - 2);
}
}
}

View file

@ -27,8 +27,16 @@ public partial class ModEditWindow
public ShpkTab(FileDialogService fileDialog, byte[] bytes)
{
FileDialog = fileDialog;
Shpk = new ShpkFile(bytes, true);
Header = $"Shader Package for DirectX {(int)Shpk.DirectXVersion}";
try
{
Shpk = new ShpkFile(bytes, true);
}
catch (NotImplementedException)
{
Shpk = new ShpkFile(bytes, false);
}
Header = $"Shader Package for DirectX {(int)Shpk.DirectXVersion}";
Extension = Shpk.DirectXVersion switch
{
ShpkFile.DxVersion.DirectX9 => ".cso",

View file

@ -1,4 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
@ -17,10 +19,12 @@ using Penumbra.GameData.Enums;
using Penumbra.GameData.Files;
using Penumbra.Import.Textures;
using Penumbra.Interop.ResourceTree;
using Penumbra.Interop.Services;
using Penumbra.Meta;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.String;
using Penumbra.String.Classes;
using Penumbra.UI.Classes;
using Penumbra.Util;
@ -42,6 +46,7 @@ public partial class ModEditWindow : Window, IDisposable
private readonly ModMergeTab _modMergeTab;
private readonly CommunicatorService _communicator;
private readonly IDragDropManager _dragDropManager;
private readonly GameEventManager _gameEvents;
private Mod? _mod;
private Vector2 _iconSize = Vector2.Zero;
@ -137,6 +142,9 @@ public partial class ModEditWindow : Window, IDisposable
{
_left.Dispose();
_right.Dispose();
_materialTab.Reset();
_modelTab.Reset();
_shaderPackageTab.Reset();
}
public override void Draw()
@ -520,10 +528,33 @@ public partial class ModEditWindow : Window, IDisposable
return new FullPath(path);
}
private HashSet<Utf8GamePath> FindPathsStartingWith(ByteString prefix)
{
var ret = new HashSet<Utf8GamePath>();
foreach (var path in _activeCollections.Current.ResolvedFiles.Keys)
{
if (path.Path.StartsWith(prefix))
ret.Add(path);
}
if (_mod != null)
foreach (var option in _mod.Groups.SelectMany(g => g).Append(_mod.Default))
{
foreach (var path in option.Files.Keys)
{
if (path.Path.StartsWith(prefix))
ret.Add(path);
}
}
return ret;
}
public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, IDataManager gameData,
Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager,
StainService stainService, ActiveCollections activeCollections, DalamudServices dalamud, ModMergeTab modMergeTab,
CommunicatorService communicator, TextureManager textures, IDragDropManager dragDropManager)
CommunicatorService communicator, TextureManager textures, IDragDropManager dragDropManager, GameEventManager gameEvents)
: base(WindowBaseLabel)
{
_performance = performance;
@ -539,14 +570,15 @@ public partial class ModEditWindow : Window, IDisposable
_dragDropManager = dragDropManager;
_textures = textures;
_fileDialog = fileDialog;
_gameEvents = gameEvents;
_materialTab = new FileEditor<MtrlTab>(this, gameData, config, _fileDialog, "Materials", ".mtrl",
() => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty,
bytes => new MtrlTab(this, new MtrlFile(bytes)));
(bytes, path, writable) => new MtrlTab(this, new MtrlFile(bytes), path, writable));
_modelTab = new FileEditor<MdlFile>(this, gameData, config, _fileDialog, "Models", ".mdl",
() => _editor.Files.Mdl, DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, bytes => new MdlFile(bytes));
() => _editor.Files.Mdl, DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, _, _) => new MdlFile(bytes));
_shaderPackageTab = new FileEditor<ShpkTab>(this, gameData, config, _fileDialog, "Shaders", ".shpk",
() => _editor.Files.Shpk, DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty,
bytes => new ShpkTab(_fileDialog, bytes));
(bytes, _, _) => new ShpkTab(_fileDialog, bytes));
_center = new CombinedTexture(_left, _right);
_textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor);
_quickImportViewer = new ResourceTreeViewer(_config, resourceTreeFactory, 2, OnQuickImportRefresh, DrawQuickImportActions);
@ -557,6 +589,9 @@ public partial class ModEditWindow : Window, IDisposable
{
_communicator.ModPathChanged.Unsubscribe(OnModPathChanged);
_editor?.Dispose();
_materialTab.Dispose();
_modelTab.Dispose();
_shaderPackageTab.Dispose();
_left.Dispose();
_right.Dispose();
_center.Dispose();

View file

@ -195,21 +195,7 @@ public class ModPanelSettingsTab : ITab
_collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, (uint)idx2);
if (option.Description.Length > 0)
{
var hovered = ImGui.IsItemHovered();
ImGui.SameLine();
using (var _ = ImRaii.PushFont(UiBuilder.IconFont))
{
using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled));
ImGuiUtil.RightAlign(FontAwesomeIcon.InfoCircle.ToIconString(), ImGui.GetStyle().ItemSpacing.X);
}
if (hovered)
{
using var tt = ImRaii.Tooltip();
ImGui.TextUnformatted(option.Description);
}
}
ImGuiUtil.SelectableHelpMarker(option.Description);
id.Pop();
}