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

@ -68,23 +68,10 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
private unsafe ResourceNode? CreateNodeFromResourceHandle(ResourceType type, nint sourceAddress, ResourceHandle* handle, bool @internal,
bool withName)
{
if (handle == null)
var fullPath = Utf8GamePath.FromByteString(GetResourceHandlePath(handle), out var p) ? new FullPath(p) : FullPath.Empty;
if (fullPath.InternalName.IsEmpty)
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 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();
}
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[], T?> _parseFile;
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,27 +13,44 @@ namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow
{
private bool DrawMaterialColorSetChange( MtrlFile file, bool disabled )
{
if( !file.ColorSets.Any( c => c.HasRows ) )
{
return false;
}
private static readonly float HalfMinValue = (float)Half.MinValue;
private static readonly float HalfMaxValue = (float)Half.MaxValue;
private static readonly float HalfEpsilon = (float)Half.Epsilon;
ColorSetCopyAllClipboardButton( file, 0 );
private bool DrawMaterialColorSetChange(MtrlTab tab, bool disabled)
{
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( file, 0 );
var ret = ColorSetPasteAllClipboardButton(tab, 0, disabled);
if (!disabled)
{
ImGui.SameLine();
ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0));
ImGui.SameLine();
ret |= DrawPreviewDye( file, disabled );
ret |= ColorSetDyeableCheckbox(tab, ref hasAnyDye);
}
using var table = ImRaii.Table( "##ColorSets", 11,
if (hasAnyDye)
{
ImGui.SameLine();
ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0));
ImGui.SameLine();
ret |= DrawPreviewDye(tab, disabled);
}
using var table = ImRaii.Table("##ColorSets", hasAnyDye ? 11 : 9,
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV);
if (!table)
{
return false;
}
ImGui.TableNextColumn();
ImGui.TableHeader(string.Empty);
@ -53,17 +70,20 @@ public partial class ModEditWindow
ImGui.TableHeader("Repeat");
ImGui.TableNextColumn();
ImGui.TableHeader("Skew");
if (hasAnyDye)
{
ImGui.TableNextColumn();
ImGui.TableHeader("Dye");
ImGui.TableNextColumn();
ImGui.TableHeader("Dye Preview");
}
for( var j = 0; j < file.ColorSets.Length; ++j )
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();
}
}
@ -75,9 +95,7 @@ public partial class ModEditWindow
private static void ColorSetCopyAllClipboardButton(MtrlFile file, int colorSetIdx)
{
if (!ImGui.Button("Export All Rows to Clipboard", ImGuiHelpers.ScaledVector2(200, 0)))
{
return;
}
try
{
@ -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.";
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 );
}
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 )
{
if( !ImGui.Button( "Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ) ) || file.ColorSets.Length <= colorSetIdx )
private static unsafe bool ColorSetPasteAllClipboardButton(MtrlTab tab, int colorSetIdx, bool disabled)
{
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>())
{
return false;
}
ref var rows = ref file.ColorSets[ colorSetIdx ].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 )
&& tab.Mtrl.ColorDyeSets.Length > colorSetIdx)
{
ref var dyeRows = ref file.ColorDyeSets[ colorSetIdx ].Rows;
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
@ -160,9 +181,10 @@ public partial class ModEditWindow
private static unsafe void ColorSetCopyClipboardButton(MtrlFile.ColorSet.Row row, MtrlFile.ColorDyeSet.Row dye)
{
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Clipboard.ToIconString(), ImGui.GetFrameHeight() * Vector2.One,
if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), ImGui.GetFrameHeight() * Vector2.One,
"Export this row to your clipboard.", false, true))
{
return;
try
{
var data = new byte[MtrlFile.ColorSet.Row.Size + 2];
@ -180,44 +202,65 @@ public partial class ModEditWindow
// ignored
}
}
private static bool ColorSetDyeableCheckbox(MtrlTab tab, ref bool dyeable)
{
var ret = ImGui.Checkbox("Dyeable", ref dyeable);
if (ret)
{
tab.UseColorDyeSet = dyeable;
if (dyeable)
tab.Mtrl.FindOrAddColorDyeSet();
tab.UpdateColorSetPreview();
}
private static unsafe bool ColorSetPasteFromClipboardButton( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled )
return ret;
}
private static unsafe bool ColorSetPasteFromClipboardButton(MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled)
{
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Paste.ToIconString(), ImGui.GetFrameHeight() * Vector2.One,
if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Paste.ToIconString(), ImGui.GetFrameHeight() * Vector2.One,
"Import an exported row from your clipboard onto this row.", disabled, true))
{
return false;
try
{
var text = ImGui.GetClipboardText();
var data = Convert.FromBase64String(text);
if (data.Length != MtrlFile.ColorSet.Row.Size + 2
|| file.ColorSets.Length <= colorSetIdx )
{
|| tab.Mtrl.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 );
}
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
{
// ignored
}
}
return false;
}
}
private bool DrawColorSetRow( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled )
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)
{
@ -226,38 +269,53 @@ public partial class ModEditWindow
}
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();
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);
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.TableNextColumn();
using var dis = ImRaii.Disabled(disabled);
ret |= ColorPicker( "##Diffuse", "Diffuse Color", row.Diffuse, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Diffuse = c );
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 );
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 ) )
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);
@ -266,28 +324,46 @@ public partial class ModEditWindow
{
ImGui.SameLine();
ret |= ImGuiUtil.Checkbox("##dyeSpecular", "Apply Specular Color on Dye", dye.Specular,
b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Specular = b, ImGuiHoveredFlags.AllowWhenDisabled );
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 );
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 );
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 );
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 ) )
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);
@ -295,16 +371,21 @@ public partial class ModEditWindow
{
ImGui.SameLine();
ret |= ImGuiUtil.Checkbox("##dyeGloss", "Apply Gloss Strength on Dye", dye.Gloss,
b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Gloss = b, ImGuiHoveredFlags.AllowWhenDisabled );
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 )
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);
@ -312,20 +393,24 @@ public partial class ModEditWindow
ImGui.TableNextColumn();
tmpFloat = row.MaterialRepeat.X;
ImGui.SetNextItemWidth(floatSize);
if( ImGui.DragFloat( "##RepeatX", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialRepeat.X ) )
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);
ImGui.SameLine();
tmpFloat = row.MaterialRepeat.Y;
ImGui.SetNextItemWidth(floatSize);
if( ImGui.DragFloat( "##RepeatY", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialRepeat.Y ) )
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);
@ -333,10 +418,11 @@ public partial class ModEditWindow
ImGui.TableNextColumn();
tmpFloat = row.MaterialSkew.X;
ImGui.SetNextItemWidth(floatSize);
if( ImGui.DragFloat( "##SkewX", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialSkew.X ) )
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);
@ -344,45 +430,46 @@ public partial class ModEditWindow
ImGui.SameLine();
tmpFloat = row.MaterialSkew.Y;
ImGui.SetNextItemWidth(floatSize);
if( ImGui.DragFloat( "##SkewY", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialSkew.Y ) )
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);
ImGui.TableNextColumn();
if (hasDye)
{
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);
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))
{
return false;
}
var values = entry[(int)stain];
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing / 2);
@ -390,7 +477,9 @@ public partial class ModEditWindow
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");
@ -401,10 +490,10 @@ public partial class ModEditWindow
ImGui.SameLine();
using var dis = ImRaii.Disabled();
ImGui.SetNextItemWidth(floatSize);
ImGui.DragFloat( "##gloss", ref values.Gloss, 0, 0, 0, "%.2f G" );
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.DragFloat("##specularStrength", ref values.SpecularPower, 0, values.SpecularPower, values.SpecularPower, "%.2f S");
return ret;
}
@ -412,12 +501,17 @@ public partial class ModEditWindow
private static bool ColorPicker(string label, string tooltip, Vector3 input, Action<Vector3> setter, string letter = "")
{
var ret = false;
var tmp = input;
var inputSqrt = PseudoSqrtRgb(input);
var tmp = inputSqrt;
if (ImGui.ColorEdit3(label, ref tmp,
ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.DisplayRGB | ImGuiColorEditFlags.InputRGB | ImGuiColorEditFlags.NoTooltip )
&& tmp != input )
ImGuiColorEditFlags.NoInputs
| ImGuiColorEditFlags.DisplayRGB
| ImGuiColorEditFlags.InputRGB
| ImGuiColorEditFlags.NoTooltip
| ImGuiColorEditFlags.HDR)
&& tmp != inputSqrt)
{
setter( tmp );
setter(PseudoSquareRgb(tmp));
ret = true;
}
@ -433,4 +527,24 @@ public partial class ModEditWindow
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,81 +1,111 @@
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 ShpkFile? AssociatedShpk;
public readonly List< string > TextureLabels = new(4);
public string ShaderHeader = "Shader###Shader";
public FullPath LoadedShpkPath = FullPath.Empty;
public string LoadedShpkPathName = string.Empty;
public float TextureLabelWidth;
public string LoadedShpkDevkitPathName = string.Empty;
public string ShaderComment = string.Empty;
public ShpkFile? AssociatedShpk;
public JObject? AssociatedShpkDevkit;
public readonly string LoadedBaseDevkitPathName;
public readonly JObject? AssociatedBaseDevkit;
// Shader Key State
public readonly List< string > 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;
// 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;
// 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();
public FullPath FindAssociatedShpk(out string defaultPath, out Utf8GamePath defaultGamePath)
{
defaultPath = GamePaths.Shader.ShpkPath(Mtrl.ShaderPackage.Name);
if (!Utf8GamePath.FromString(defaultPath, out defaultGamePath, true))
{
return FullPath.Empty;
}
return _edit.FindBestMatch(defaultGamePath);
}
public string[] GetShpkNames()
{
if (null != _shpkNames)
return _shpkNames;
var names = new HashSet<string>(StandardShaderPackages);
names.UnionWith(_edit.FindPathsStartingWith(ShpkPrefix).Select(path => path.ToString()[ShpkPrefixLength..]));
_shpkNames = names.ToArray();
Array.Sort(_shpkNames);
return _shpkNames;
}
public void LoadShpk(FullPath path)
{
ShaderHeader = $"Shader ({Mtrl.ShaderPackage.Name})###Shader";
try
{
LoadedShpkPath = path;
@ -90,204 +120,664 @@ public partial class ModEditWindow
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();
try
{
if (!Utf8GamePath.FromString("penumbra/shpk_devkit/" + shpkBaseName + ".json", out var devkitPath))
throw new Exception("Could not assemble ShPk dev-kit path.");
var devkitFullPath = _edit.FindBestMatch(devkitPath);
if (!devkitFullPath.IsRooted)
throw new Exception("Could not resolve ShPk dev-kit path.");
devkitPathName = devkitFullPath.FullName;
return JObject.Parse(File.ReadAllText(devkitFullPath.FullName));
}
catch
{
devkitPathName = string.Empty;
return null;
}
}
private T? TryGetShpkDevkitData<T>(string category, uint? id, bool mayVary) where T : class
=> TryGetShpkDevkitData<T>(AssociatedShpkDevkit, LoadedShpkDevkitPathName, category, id, mayVary)
?? TryGetShpkDevkitData<T>(AssociatedBaseDevkit, LoadedBaseDevkitPathName, category, id, mayVary);
private T? TryGetShpkDevkitData<T>(JObject? devkit, string devkitPathName, string category, uint? id, bool mayVary) where T : class
{
if (devkit == null)
return null;
try
{
var data = devkit[category];
if (id.HasValue)
data = data?[id.Value.ToString()];
if (mayVary && (data as JObject)?["Vary"] != null)
{
var selector = BuildSelector(data["Vary"]!
.Select(key => (uint)key)
.Select(key => Mtrl.GetShaderKey(key)?.Value ?? AssociatedShpk!.GetMaterialKeyById(key)!.Value.DefaultValue));
var index = (int)data["Selectors"]![selector.ToString()]!;
data = data["Items"]![index];
}
return data?.ToObject(typeof(T)) as T;
}
catch (Exception e)
{
// Some element in the JSON was undefined or invalid (wrong type, key that doesn't exist in the ShPk, index out of range, …)
Penumbra.Log.Error($"Error while traversing the ShPk dev-kit file at {devkitPathName}: {e}");
return null;
}
}
private void UpdateShaderKeys()
{
ShaderKeys.Clear();
if (AssociatedShpk != null)
foreach (var key in AssociatedShpk.MaterialKeys)
{
var dkData = TryGetShpkDevkitData<DevkitShaderKey>("ShaderKeys", key.Id, false);
var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label);
var valueSet = new HashSet<uint>(key.Values);
if (dkData != null)
valueSet.UnionWith(dkData.Values.Keys);
var mtrlKeyIndex = Mtrl.FindOrAddShaderKey(key.Id, key.DefaultValue);
var values = valueSet.Select<uint, (string Label, uint Value, string Description)>(value =>
{
if (dkData != null && dkData.Values.TryGetValue(value, out var dkValue))
return (dkValue.Label.Length > 0 ? dkValue.Label : $"0x{value:X8}", value, dkValue.Description);
return ($"0x{value:X8}", value, string.Empty);
}).ToArray();
Array.Sort(values, (x, y) =>
{
if (x.Value == key.DefaultValue)
return -1;
if (y.Value == key.DefaultValue)
return 1;
return string.Compare(x.Label, y.Label, StringComparison.Ordinal);
});
ShaderKeys.Add((hasDkLabel ? dkData!.Label : $"0x{key.Id:X8}", mtrlKeyIndex, dkData?.Description ?? string.Empty,
!hasDkLabel, values));
}
else
foreach (var (key, index) in Mtrl.ShaderPackage.ShaderKeys.WithIndex())
ShaderKeys.Add(($"0x{key.Category:X8}", index, string.Empty, true, Array.Empty<(string, uint, string)>()));
}
private void UpdateShaders()
{
VertexShaders.Clear();
PixelShaders.Clear();
if (AssociatedShpk == null)
{
ShadersKnown = false;
}
else
{
ShadersKnown = true;
var systemKeySelectors = AllSelectors(AssociatedShpk.SystemKeys).ToArray();
var sceneKeySelectors = AllSelectors(AssociatedShpk.SceneKeys).ToArray();
var subViewKeySelectors = AllSelectors(AssociatedShpk.SubViewKeys).ToArray();
var materialKeySelector =
BuildSelector(AssociatedShpk.MaterialKeys.Select(key => Mtrl.GetOrAddShaderKey(key.Id, key.DefaultValue).Value));
foreach (var systemKeySelector in systemKeySelectors)
{
foreach (var sceneKeySelector in sceneKeySelectors)
{
foreach (var subViewKeySelector in subViewKeySelectors)
{
var selector = BuildSelector(systemKeySelector, sceneKeySelector, materialKeySelector, subViewKeySelector);
var node = AssociatedShpk.GetNodeBySelector(selector);
if (node.HasValue)
foreach (var pass in node.Value.Passes)
{
VertexShaders.Add((int)pass.VertexShader);
PixelShaders.Add((int)pass.PixelShader);
}
else
ShadersKnown = false;
}
}
}
}
var vertexShaders = VertexShaders.OrderBy(i => i).Select(i => $"#{i}");
var pixelShaders = PixelShaders.OrderBy(i => i).Select(i => $"#{i}");
VertexShadersString = $"Vertex Shaders: {string.Join(", ", ShadersKnown ? vertexShaders : vertexShaders.Append("???"))}";
PixelShadersString = $"Pixel Shaders: {string.Join(", ", ShadersKnown ? pixelShaders : pixelShaders.Append("???"))}";
ShaderComment = TryGetShpkDevkitData<string>("Comment", null, true) ?? string.Empty;
}
private void UpdateTextures()
{
Textures.Clear();
SamplerIds.Clear();
if (AssociatedShpk == null)
{
SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId));
if (Mtrl.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))
{
for( var i = 0; i < Mtrl.Textures.Length; ++i )
foreach (var (label, _, _, description, monoFont) in Textures)
{
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 );
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 );
foreach (var (groupName, group) in groups)
{
if (string.Equals(name, groupName, StringComparison.Ordinal))
return group;
}
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 ) )
{
var key = AssociatedShpk.MaterialKeys[ MissingShaderKeyIndices[ 0 ] ];
NewKeyId = key.Id;
NewKeyDefault = key.DefaultValue;
var newGroup = new List<T>(16);
groups.Add((name, newGroup));
return newGroup;
}
AvailableKeyValues.AddRange( AssociatedShpk.MaterialKeys.Select( k => DefinedShaderKeys.TryGetValue( k.Id, out var value ) ? value : k.DefaultValue ) );
foreach( var node in AssociatedShpk.Nodes )
Constants.Clear();
if (AssociatedShpk == null)
{
if( node.MaterialKeys.WithIndex().All( key => key.Value == AvailableKeyValues[ key.Index ] ) )
var fcGroup = FindOrAddGroup(Constants, "Further Constants");
foreach (var (constant, index) in Mtrl.ShaderPackage.Constants.WithIndex())
{
foreach( var pass in node.Passes )
{
vertexShaders.Add( ( int )pass.VertexShader );
pixelShaders.Add( ( int )pass.PixelShader );
}
}
}
}
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() )
{
DefinedMaterialConstants.Add( constant.Id );
var values = Mtrl.GetConstantValues(constant);
var paramValueOffset = -values.Length;
if( values.Length > 0 )
for (var i = 0; i < values.Length; i += 4)
{
var shpkParam = AssociatedShpk?.GetMaterialParamById( constant.Id );
var paramByteOffset = shpkParam?.ByteOffset ?? -1;
if( ( paramByteOffset & 0x3 ) == 0 )
{
paramValueOffset = paramByteOffset >> 2;
fcGroup.Add(($"0x{constant.Id:X8}", index, i..Math.Min(i + 4, values.Length), string.Empty, true,
FloatConstantEditor.Default));
}
}
var unique = OrphanedMaterialValues.RemoveRange( constant.ByteOffset >> 2, values.Length );
AliasedMaterialValueCount += values.Length - unique;
}
else
{
HasMalformedMaterialConstants = true;
var prefix = AssociatedShpk.GetConstantById(MaterialParamsConstantId)?.Name ?? string.Empty;
foreach (var shpkConstant in AssociatedShpk.MaterialParams)
{
if ((shpkConstant.ByteSize & 0x3) != 0)
continue;
var constant = Mtrl.GetOrAddConstant(shpkConstant.Id, shpkConstant.ByteSize >> 2, out var constantIndex);
var values = Mtrl.GetConstantValues(constant);
var handledElements = new IndexSet(values.Length, false);
var dkData = TryGetShpkDevkitData<DevkitConstant[]>("Constants", shpkConstant.Id, true);
if (dkData != null)
foreach (var dkConstant in dkData)
{
var offset = (int)dkConstant.Offset;
var length = values.Length - offset;
if (dkConstant.Length.HasValue)
length = Math.Min(length, (int)dkConstant.Length.Value);
if (length <= 0)
continue;
var editor = dkConstant.CreateEditor();
if (editor != null)
FindOrAddGroup(Constants, dkConstant.Group.Length > 0 ? dkConstant.Group : "Further Constants")
.Add((dkConstant.Label, constantIndex, offset..(offset + length), dkConstant.Description, false, editor));
handledElements.AddRange(offset, length);
}
var (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 ) );
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));
}
MissingMaterialConstants.Clear();
if( AssociatedShpk != null )
{
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 )
else
{
NewConstantIdx = 0;
NewConstantId = MissingMaterialConstants[ 0 ].Id;
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));
}
}
}
}
}
public void UpdateSamplers()
Constants.RemoveAll(group => group.Constants.Count == 0);
Constants.Sort((x, y) =>
{
Samplers.Clear();
DefinedSamplers.Clear();
OrphanedSamplers = new IndexSet( Mtrl.Textures.Length, true );
foreach( var (sampler, idx) in Mtrl.ShaderPackage.Samplers.WithIndex() )
if (string.Equals(x.Header, "Further Constants", StringComparison.Ordinal))
return 1;
if (string.Equals(y.Header, "Further Constants", StringComparison.Ordinal))
return -1;
return string.Compare(x.Header, y.Header, StringComparison.Ordinal);
});
// HACK the Replace makes w appear after xyz, for the cbuffer-location-based naming scheme
foreach (var (_, group) in Constants)
{
DefinedSamplers.Add( sampler.SamplerId );
if( !OrphanedSamplers.Remove( sampler.TextureIndex ) )
{
++AliasedSamplerCount;
group.Sort((x, y) => string.CompareOrdinal(
x.MonoFont ? x.Label.Replace("].w", "].{") : x.Label,
y.MonoFont ? y.Label.Replace("].w", "].{") : y.Label));
}
}
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 ) );
public unsafe void BindToMaterialInstances()
{
UnbindFromMaterialInstances();
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 )
ColorSetPreviewers.Add(new LiveColorSetPreviewer(_edit._dalamud.Objects, _edit._dalamud.Framework, materialInfo));
}
catch (InvalidOperationException)
{
setSampler = true;
NewSamplerIdx = MissingSamplers.Count;
// Carry on without that previewer.
}
}
MissingSamplers.Add( ( sampler.Name, sampler.Id ) );
UpdateColorSetPreview();
}
if( !setSampler && MissingSamplers.Count > 0 )
private void UnbindFromMaterialInstances()
{
NewSamplerIdx = 0;
NewSamplerId = MissingSamplers[ 0 ].Id;
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;
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[]
{
"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)
{
ImGui.TextUnformatted("Shader Package: " + tab.Mtrl.ShaderPackage.Name);
return false;
}
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))
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;
}
if (ImGui.IsItemDeactivatedAfterEdit())
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)
private static bool DrawMaterialShaderKeys(MtrlTab tab, bool disabled)
{
var ret = false;
using var t2 = ImRaii.TreeNode(tab.ShaderKeyLabels[idx], disabled ? ImGuiTreeNodeFlags.Leaf : 0);
if (!t2 || disabled)
return ret;
if (tab.ShaderKeys.Count == 0)
return false;
var key = tab.Mtrl.ShaderPackage.ShaderKeys[idx];
var ret = false;
foreach (var (label, index, description, monoFont, values) in tab.ShaderKeys)
{
using var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont);
ref var key = ref tab.Mtrl.ShaderPackage.ShaderKeys[index];
var shpkKey = tab.AssociatedShpk?.GetMaterialKeyById(key.Category);
if (shpkKey.HasValue)
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 * 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}"))
ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f);
using (var c = ImRaii.Combo($"##{key.Category:X8}", currentLabel))
{
if (c)
foreach (var idx in tab.MissingShaderKeyIndices)
foreach (var (valueLabel, value, valueDescription) in values)
{
var key = tab.AssociatedShpk!.MaterialKeys[idx];
if (ImGui.Selectable($"ID: 0x{key.Id:X8}", key.Id == tab.NewKeyId))
if (ImGui.Selectable(valueLabel, value == currentValue))
{
tab.NewKeyDefault = key.DefaultValue;
tab.NewKeyId = key.Id;
key.Value = value;
ret = true;
tab.UpdateShaderKeyLabels();
tab.Update();
}
if (valueDescription.Length > 0)
ImGuiUtil.SelectableHelpMarker(valueDescription);
}
}
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();
if (description.Length > 0)
ImGuiUtil.LabeledHelpMarker(label, description);
else
ImGui.TextUnformatted(label);
}
return ret;
}
private static bool DrawMaterialShaderKeys(MtrlTab tab, bool disabled)
else if (description.Length > 0 || currentDescription.Length > 0)
{
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)
return false;
var ret = false;
for (var idx = 0; idx < tab.Mtrl.ShaderPackage.ShaderKeys.Length; ++idx)
ret |= DrawShaderKey(tab, disabled, ref idx);
if (!disabled && tab.AssociatedShpk != null && tab.MissingShaderKeyIndices.Count != 0)
ret |= DrawNewShaderKey(tab);
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)
if (tab.ShaderComment.Length > 0)
{
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)
{
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];
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;
sampler.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();
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.Mtrl, disabled);
ret |= DrawShaderFlagsInput(tab, disabled);
DrawCustomAssociations(tab);
ret |= DrawMaterialShaderKeys(tab, disabled);
DrawMaterialShaders(tab);
ret |= DrawMaterialConstants(tab, disabled);
ret |= DrawMaterialSamplers(tab, disabled);
}
if (tab.AssociatedShpkDevkit == null)
{
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
GC.KeepAlive(tab);
var textColor = ImGui.GetColorU32(ImGuiCol.Text);
var textColorWarning =
(textColor & 0xFF000000u)
| ((textColor & 0x00FEFEFE) >> 1)
| (tab.AssociatedShpk == null ? 0x80u : 0x8080u); // Half red or yellow
using var c = ImRaii.PushColor(ImGuiCol.Text, textColorWarning);
ImGui.TextUnformatted(tab.AssociatedShpk == null
? "Unable to find a suitable .shpk file for cross-references. Some functionality will be missing."
: "No dev-kit file found for this material's shaders. Please install one for optimal editing experience, such as actual constant names instead of hexadecimal identifiers.");
}
return ret;
}
@ -495,9 +443,7 @@ public partial class ModEditWindow
};
}
private static (string? Name, bool ComponentOnly) MaterialParamRangeName(string prefix, int valueOffset, int valueLength)
{
static string VectorSwizzle(int firstComponent, int lastComponent)
private static string VectorSwizzle(int firstComponent, int lastComponent)
=> (firstComponent, lastComponent) switch
{
(0, 4) => " ",
@ -514,6 +460,8 @@ public partial class ModEditWindow
_ => string.Empty,
};
private static (string? Name, bool ComponentOnly) MaterialParamRangeName(string prefix, int valueOffset, int valueLength)
{
if (valueLength == 0 || valueOffset < 0)
return (null, false);

View file

@ -6,6 +6,7 @@ using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Files;
using Penumbra.String.Classes;
using Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow;
@ -15,16 +16,17 @@ public partial class ModEditWindow
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 );
var ret = DrawBackFaceAndTransparency(tab, disabled);
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
ret |= DrawMaterialColorSetChange( tab.Mtrl, disabled );
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);
@ -32,37 +34,90 @@ public partial class ModEditWindow
return !disabled && ret;
}
private static void DrawMaterialLivePreviewRebind(MtrlTab tab, bool disabled)
{
if (disabled)
return;
if (ImGui.Button("Reload live preview"))
tab.BindToMaterialInstances();
if (tab.MaterialPreviewers.Count != 0 || tab.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", 2 );
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);
for( var i = 0; i < tab.Mtrl.Textures.Length; ++i )
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();
if (ImGuiUtil.DrawDisabledButton((unfolded ? FontAwesomeIcon.CaretDown : FontAwesomeIcon.CaretRight).ToIconString(),
new Vector2(frameHeight),
"Settings for this texture and the associated sampler", false, true))
{
unfolded = !unfolded;
if (unfolded)
tab.UnfoldedTextures.Add(samplerI);
else
tab.UnfoldedTextures.Remove(samplerI);
}
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
if (ImGui.InputText(string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength,
disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)
&& tmp.Length > 0
&& tmp != tab.Mtrl.Textures[ i ].Path )
&& tmp != tab.Mtrl.Textures[textureI].Path)
{
ret = true;
tab.Mtrl.Textures[ i ].Path = tmp;
tab.Mtrl.Textures[textureI].Path = tmp;
}
ImGui.TableNextColumn();
using var font = ImRaii.PushFont( UiBuilder.MonoFont );
using (var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont))
{
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted( tab.TextureLabels[ i ] );
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;
@ -71,19 +126,22 @@ public partial class ModEditWindow
using var dis = ImRaii.Disabled(disabled);
var tmp = ( file.ShaderPackage.Flags & transparencyBit ) != 0;
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;
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;
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;
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;
@ -92,45 +150,31 @@ public partial class ModEditWindow
private static void DrawOtherMaterialDetails(MtrlFile file, bool _)
{
if (!ImGui.CollapsingHeader("Further Content"))
{
return;
}
using (var sets = ImRaii.TreeNode("UV Sets", ImGuiTreeNodeFlags.DefaultOpen))
{
if (sets)
{
foreach (var set in file.UvSets)
{
ImRaii.TreeNode($"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf).Dispose();
}
}
}
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}")));
}
}
private void DrawMaterialReassignmentTab()
{
if (_editor.Files.Mdl.Count == 0)
{
return;
}
using var tab = ImRaii.TabItem("Material Reassignment");
if (!tab)
{
return;
}
ImGui.NewLine();
MaterialSuffix.Draw(_editor, ImGuiHelpers.ScaledVector2(175, 0));
@ -138,15 +182,11 @@ public partial class ModEditWindow
ImGui.NewLine();
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)
{
return;
}
var iconSize = ImGui.GetFrameHeight() * Vector2.One;
foreach (var (info, idx) in _editor.MdlMaterialEditor.ModelFiles.WithIndex())
@ -155,16 +195,12 @@ public partial class ModEditWindow
ImGui.TableNextColumn();
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))
{
info.Restore();
}
ImGui.TableNextColumn();
ImGui.TextUnformatted(info.Path.FullName[(_mod!.ModPath.FullName.Length + 1)..]);
@ -172,9 +208,7 @@ public partial class ModEditWindow
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)
{
@ -185,10 +219,8 @@ public partial class ModEditWindow
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,7 +14,6 @@ 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;
@ -40,7 +39,13 @@ public partial class ModEditWindow
ret |= DrawShaderPackageMaterialParamLayout(file, disabled);
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
ret |= DrawOtherShaderPackageDetails( file, disabled );
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();
@ -50,14 +55,23 @@ public partial class ModEditWindow
}
private static void DrawShaderPackageSummary(ShpkTab tab)
=> ImGui.TextUnformatted( tab.Header );
{
ImGui.TextUnformatted(tab.Header);
if (!tab.Shpk.Disassembled)
{
var textColor = ImGui.GetColorU32(ImGuiCol.Text);
var textColorWarning = (textColor & 0xFF000000u) | ((textColor & 0x00FEFEFE) >> 1) | 0x80u; // Half red
using var c = ImRaii.PushColor(ImGuiCol.Text, textColorWarning);
ImGui.TextUnformatted("Your system doesn't support disassembling shaders. Some functionality will be missing.");
}
}
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
{
@ -67,12 +81,11 @@ public partial class ModEditWindow
};
var blob = shader.Blob;
tab.FileDialog.OpenSavePicker( $"Export {objectName} #{idx} Program Blob to...", tab.Extension, defaultName, tab.Extension, ( success, name ) =>
tab.FileDialog.OpenSavePicker($"Export {objectName} #{idx} Program Blob to...", tab.Extension, defaultName, tab.Extension,
(success, name) =>
{
if (!success)
{
return;
}
try
{
@ -80,12 +93,14 @@ public partial class ModEditWindow
}
catch (Exception e)
{
Penumbra.Chat.NotificationMessage( $"Could not export {defaultName}{tab.Extension} to {name}:\n{e.Message}", "Penumbra Advanced Editing",
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.Chat.NotificationMessage(
$"Shader Program Blob {defaultName}{tab.Extension} exported successfully to {Path.GetFileName(name)}",
"Penumbra Advanced Editing", NotificationType.Success);
}, null, false);
}
@ -93,16 +108,13 @@ public partial class ModEditWindow
private static void DrawShaderImportButton(ShpkTab tab, string objectName, Shader[] shaders, int idx)
{
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 ) =>
tab.FileDialog.OpenFilePicker($"Replace {objectName} #{idx} Program Blob...", "Shader Program Blobs{.o,.cso,.dxbc,.dxil}",
(success, name) =>
{
if (!success)
{
return;
}
try
{
@ -110,7 +122,8 @@ public partial class ModEditWindow
}
catch (Exception e)
{
Penumbra.Chat.NotificationMessage( $"Could not import {name}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error );
Penumbra.Chat.NotificationMessage($"Could not import {name}:\n{e.Message}", "Penumbra Advanced Editing",
NotificationType.Error);
return;
}
@ -122,7 +135,8 @@ public partial class ModEditWindow
catch (Exception e)
{
tab.Shpk.SetInvalid();
Penumbra.Chat.NotificationMessage( $"Failed to update resources after importing {name}:\n{e.Message}", "Penumbra Advanced Editing",
Penumbra.Chat.NotificationMessage($"Failed to update resources after importing {name}:\n{e.Message}",
"Penumbra Advanced Editing",
NotificationType.Error);
return;
}
@ -135,22 +149,19 @@ public partial class ModEditWindow
{
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,
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)
{
if (shaders.Length == 0 || !ImGui.CollapsingHeader($"{objectName}s"))
{
return false;
}
var ret = false;
for (var idx = 0; idx < shaders.Length; ++idx)
@ -158,12 +169,10 @@ public partial class ModEditWindow
var shader = shaders[idx];
using var t = ImRaii.TreeNode($"{objectName} #{idx}");
if (!t)
{
continue;
}
DrawShaderExportButton(tab, objectName, shader, idx);
if( !disabled )
if (!disabled && tab.Shpk.Disassembled)
{
ImGui.SameLine();
DrawShaderImportButton(tab, objectName, shaders, idx);
@ -177,11 +186,10 @@ public partial class ModEditWindow
{
using var t2 = ImRaii.TreeNode($"Additional Header (Size: {shader.AdditionalHeader.Length})###AdditionalHeader");
if (t2)
{
ImGuiUtil.TextWrapped(string.Join(' ', shader.AdditionalHeader.Select(c => $"{c:X2}")));
}
}
if (tab.Shpk.Disassembled)
DrawRawDisassembly(shader);
}
@ -195,25 +203,17 @@ public partial class ModEditWindow
{
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)
{
return ret;
}
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();
}
return ret;
}
@ -221,15 +221,11 @@ public partial class ModEditWindow
private static bool DrawShaderPackageResourceArray(string arrayName, string slotLabel, bool withSize, Resource[] resources, bool disabled)
{
if (resources.Length == 0)
{
return false;
}
using var t = ImRaii.TreeNode(arrayName);
if (!t)
{
return false;
}
var ret = false;
for (var idx = 0; idx < resources.Length; ++idx)
@ -241,10 +237,8 @@ public partial class ModEditWindow
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);
}
}
return ret;
}
@ -253,20 +247,21 @@ public partial class ModEditWindow
{
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" );
ImGui.GetWindowDrawList()
.AddText(UiBuilder.DefaultFont, UiBuilder.DefaultFont.FontSize, pos, ImGui.GetColorU32(ImGuiCol.Text), "Layout");
return ret;
}
private static bool DrawMaterialParamLayoutBufferSize(ShpkFile file, Resource? materialParams)
{
var isSizeWellDefined = ( file.MaterialParamsSize & 0xF ) == 0 && ( !materialParams.HasValue || file.MaterialParamsSize == materialParams.Value.Size << 4 );
var isSizeWellDefined = (file.MaterialParamsSize & 0xF) == 0
&& (!materialParams.HasValue || file.MaterialParamsSize == materialParams.Value.Size << 4);
if (isSizeWellDefined)
{
return true;
}
ImGui.TextUnformatted(materialParams.HasValue
? $"Buffer size mismatch: {file.MaterialParamsSize} bytes ≠ {materialParams.Value.Size} registers ({materialParams.Value.Size << 4} bytes)"
@ -276,14 +271,14 @@ public partial class ModEditWindow
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)
{
return false;
}
ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, 25 * UiHelpers.Scale);
ImGui.TableSetupColumn("x", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale);
@ -333,11 +328,9 @@ public partial class ModEditWindow
}
if (deletable)
{
ImGuiUtil.HoverTooltip("\nControl + Right-Click to remove.");
}
}
}
return ret;
}
@ -346,16 +339,12 @@ public partial class ModEditWindow
{
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();
}
}
private static void DrawShaderPackageStartCombo(ShpkTab tab)
{
@ -365,16 +354,12 @@ public partial class ModEditWindow
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);
}
}
}
}
ImGui.SameLine();
ImGui.TextUnformatted("Start");
@ -394,17 +379,13 @@ public partial class ModEditWindow
{
var next = tab.Orphans[i];
if (current++ != next.Index)
{
break;
}
if (ImGui.Selectable(next.Name, i == tab.NewMaterialParamEnd))
{
tab.NewMaterialParamEnd = i;
}
}
}
}
ImGui.SameLine();
ImGui.TextUnformatted("End");
@ -413,27 +394,22 @@ public partial class ModEditWindow
private static bool DrawShaderPackageNewParameter(ShpkTab tab)
{
if (tab.Orphans.Count == 0)
{
return false;
}
DrawShaderPackageStartCombo(tab);
DrawShaderPackageEndCombo(tab);
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)
? "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,
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
{
@ -451,38 +427,42 @@ public partial class ModEditWindow
var materialParams = tab.Shpk.GetConstantById(MaterialParamsConstantId);
if (!DrawMaterialParamLayoutHeader(materialParams?.Name ?? "Material Parameter"))
{
return false;
}
var sizeWellDefined = DrawMaterialParamLayoutBufferSize(tab.Shpk, materialParams);
ret |= DrawShaderPackageMaterialMatrix(tab, disabled);
if (tab.MalformedParameters.Count > 0)
{
DrawShaderPackageMisalignedParameters(tab);
}
else if (!disabled && sizeWellDefined)
{
ret |= DrawShaderPackageNewParameter(tab);
return ret;
}
private static bool DrawShaderPackageResources(ShpkTab tab, bool disabled)
{
var ret = false;
if (!ImGui.CollapsingHeader("Shader Resources"))
return false;
ret |= DrawShaderPackageResourceArray("Constant Buffers", "type", true, tab.Shpk.Constants, disabled);
ret |= DrawShaderPackageResourceArray("Samplers", "type", false, tab.Shpk.Samplers, disabled);
ret |= DrawShaderPackageResourceArray("Unordered Access Views", "type", false, tab.Shpk.Uavs, disabled);
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())
@ -500,46 +480,36 @@ public partial class ModEditWindow
private static void DrawShaderPackageNodes(ShpkTab tab)
{
if (tab.Shpk.Nodes.Length <= 0)
{
return;
}
using var t = ImRaii.TreeNode($"Nodes ({tab.Shpk.Nodes.Length})###Nodes");
if (!t)
{
return;
}
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}" );
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();
}
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();
}
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();
}
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();
}
ImRaii.TreeNode( $"Pass Indices: {string.Join( ' ', node.PassIndices.Select( c => $"{c:X2}" ) )}", 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 #{passIdx}: ID: 0x{pass.Id:X8}, Vertex Shader #{pass.VertexShader}, Pixel Shader #{pass.PixelShader}",
@ -549,20 +519,10 @@ public partial class ModEditWindow
}
}
private static bool DrawOtherShaderPackageDetails( ShpkTab tab, bool disabled )
private static void DrawShaderPackageSelection(ShpkTab tab)
{
var ret = false;
if( !ImGui.CollapsingHeader( "Further Content" ) )
{
return false;
}
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 );
if (!ImGui.CollapsingHeader("Shader Selection"))
return;
DrawKeyArray("System Keys", true, tab.Shpk.SystemKeys);
DrawKeyArray("Scene Keys", true, tab.Shpk.SceneKeys);
@ -570,31 +530,31 @@ public partial class ModEditWindow
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" );
using var t = ImRaii.TreeNode($"Node Selectors ({tab.Shpk.NodeSelectors.Count})###NodeSelectors");
if (t)
{
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
foreach( var (item, idx) in tab.Shpk.Items.WithIndex() )
foreach (var selector in tab.Shpk.NodeSelectors)
ImRaii.TreeNode($"#{selector.Value:D4}: Selector: 0x{selector.Key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet)
.Dispose();
}
}
private static void DrawOtherShaderPackageDetails(ShpkTab tab)
{
ImRaii.TreeNode( $"#{idx:D4}: ID: 0x{item.Id:X8}, node: {item.Node}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose();
}
}
}
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)
{
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)
{
var sb = new StringBuilder(256);
@ -611,9 +571,7 @@ public partial class ModEditWindow
default:
sb.Append($"[{i}].");
foreach (var c in components.ToString().Where(char.IsUpper))
{
sb.Append(char.ToLower(c));
}
sb.Append(", ");
break;
@ -629,9 +587,7 @@ public partial class ModEditWindow
default:
sb.Append("[*].");
foreach (var c in resource.UsedDynamically!.Value.ToString().Where(char.IsUpper))
{
sb.Append(char.ToLower(c));
}
sb.Append(", ");
break;
@ -641,25 +597,17 @@ public partial class ModEditWindow
{
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.Z) != 0)
{
sb.Append("Blue, ");
}
if ((components & DisassembledShader.VectorComponents.W) != 0)
{
sb.Append("Alpha, ");
}
}
return sb.Length == 0 ? string.Empty : sb.ToString(0, sb.Length - 2);
}

View file

@ -27,7 +27,15 @@ public partial class ModEditWindow
public ShpkTab(FileDialogService fileDialog, byte[] bytes)
{
FileDialog = fileDialog;
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
{

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();
}