Material editor: live-preview changes

This commit is contained in:
Exter-N 2023-08-19 05:42:26 +02:00
parent ccca2f1434
commit f64fdd2b26
14 changed files with 1067 additions and 110 deletions

View file

@ -65,26 +65,34 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
private ResourceNode CreateNodeFromGamePath(ResourceType type, nint sourceAddress, Utf8GamePath gamePath, bool @internal)
=> new(null, type, sourceAddress, gamePath, FilterFullPath(Collection.ResolvePath(gamePath) ?? new FullPath(gamePath)), @internal);
private unsafe ResourceNode? CreateNodeFromResourceHandle(ResourceType type, nint sourceAddress, ResourceHandle* handle, bool @internal,
bool withName)
public static unsafe FullPath GetResourceHandlePath(ResourceHandle* handle)
{
if (handle == null)
return null;
var name = handle->FileName();
if (name.IsEmpty)
return null;
return FullPath.Empty;
if (name[0] == (byte)'|')
{
var pos = name.IndexOf((byte)'|', 1);
if (pos < 0)
return null;
return FullPath.Empty;
name = name.Substring(pos + 1);
}
var fullPath = new FullPath(Utf8GamePath.FromByteString(name, out var p) ? p : Utf8GamePath.Empty);
return new FullPath(Utf8GamePath.FromByteString(name, out var p) ? p : Utf8GamePath.Empty);
}
private unsafe ResourceNode? CreateNodeFromResourceHandle(ResourceType type, nint sourceAddress, ResourceHandle* handle, bool @internal,
bool withName)
{
if (handle == null)
return null;
var fullPath = GetResourceHandlePath(handle);
if (fullPath.InternalName.IsEmpty)
return null;
var gamePaths = Collection.ReverseResolvePath(fullPath).ToList();
fullPath = FilterFullPath(fullPath);
@ -161,7 +169,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;

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

@ -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

@ -8,8 +8,11 @@ public unsafe struct MtrlResource
[FieldOffset( 0x00 )]
public ResourceHandle Handle;
[FieldOffset( 0xC8 )]
public ShaderPackageResourceHandle* ShpkResourceHandle;
[FieldOffset( 0xD0 )]
public ushort* TexSpace; // Contains the offsets for the tex files inside the string list.
public TextureEntry* TexSpace; // Contains the offsets for the tex files inside the string list.
[FieldOffset( 0xE0 )]
public byte* StringList;
@ -24,8 +27,21 @@ public unsafe struct MtrlResource
=> StringList + ShpkOffset;
public byte* TexString( int idx )
=> StringList + *( TexSpace + 4 + idx * 8 );
=> StringList + TexSpace[idx].PathOffset;
public bool TexIsDX11( int idx )
=> *(TexSpace + 5 + idx * 8) >= 0x8000;
=> TexSpace[idx].Flags >= 0x8000;
[StructLayout(LayoutKind.Explicit, Size = 0x10)]
public struct TextureEntry
{
[FieldOffset( 0x00 )]
public TextureResourceHandle* ResourceHandle;
[FieldOffset( 0x08 )]
public ushort PathOffset;
[FieldOffset( 0x0A )]
public ushort Flags;
}
}

View file

@ -1,5 +1,7 @@
using System;
using System.Runtime.InteropServices;
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
using FFXIVClientStructs.FFXIV.Client.System.Resource;
using Penumbra.GameData;
using Penumbra.GameData.Enums;
@ -18,12 +20,22 @@ public unsafe struct TextureResourceHandle
public IntPtr Unk;
[FieldOffset( 0x118 )]
public IntPtr KernelTexture;
public Texture* KernelTexture;
[FieldOffset( 0x20 )]
public IntPtr NewKernelTexture;
}
[StructLayout(LayoutKind.Explicit)]
public unsafe struct ShaderPackageResourceHandle
{
[FieldOffset( 0x0 )]
public ResourceHandle Handle;
[FieldOffset( 0xB0 )]
public ShaderPackage* ShaderPackage;
}
[StructLayout( LayoutKind.Explicit )]
public unsafe struct ResourceHandle
{

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;
@ -39,6 +39,11 @@ public class FileEditor<T> where T : class, IWritable
_combo = new Combo(config, getFiles);
}
~FileEditor()
{
DoDispose();
}
public void Draw()
{
using var tab = ImRaii.TabItem(_tabName);
@ -60,11 +65,23 @@ public class FileEditor<T> where T : class, IWritable
DrawFilePanel();
}
private readonly string _tabName;
private readonly string _fileType;
private readonly Func<T, bool, bool> _drawEdit;
private readonly Func<string> _getInitialPath;
private readonly Func<byte[], T?> _parseFile;
public void Dispose()
{
DoDispose();
GC.SuppressFinalize(this);
}
private void DoDispose()
{
(_currentFile as IDisposable)?.Dispose();
_currentFile = null;
}
private readonly string _tabName;
private readonly string _fileType;
private readonly Func<T, bool, bool> _drawEdit;
private readonly Func<string> _getInitialPath;
private readonly Func<byte[], string, bool, T?> _parseFile;
private FileRegistry? _currentPath;
private T? _currentFile;
@ -99,7 +116,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 +177,7 @@ public class FileEditor<T> where T : class, IWritable
{
_currentException = null;
_currentPath = null;
(_currentFile as IDisposable)?.Dispose();
_currentFile = null;
_changed = false;
}
@ -181,10 +201,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,20 +13,20 @@ namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow
{
private bool DrawMaterialColorSetChange( MtrlFile file, bool disabled )
private bool DrawMaterialColorSetChange( MtrlTab tab, bool disabled )
{
if( !file.ColorSets.Any( c => c.HasRows ) )
if( !tab.Mtrl.ColorSets.Any( c => c.HasRows ) )
{
return false;
}
ColorSetCopyAllClipboardButton( file, 0 );
ColorSetCopyAllClipboardButton( tab.Mtrl, 0 );
ImGui.SameLine();
var ret = ColorSetPasteAllClipboardButton( file, 0 );
var ret = ColorSetPasteAllClipboardButton( tab, 0 );
ImGui.SameLine();
ImGui.Dummy( ImGuiHelpers.ScaledVector2( 20, 0 ) );
ImGui.SameLine();
ret |= DrawPreviewDye( file, disabled );
ret |= DrawPreviewDye( tab, disabled );
using var table = ImRaii.Table( "##ColorSets", 11,
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV );
@ -58,12 +58,12 @@ public partial class ModEditWindow
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 );
ImGui.TableNextRow();
}
}
@ -95,33 +95,36 @@ public partial class ModEditWindow
}
}
private bool DrawPreviewDye( MtrlFile file, bool disabled )
private bool DrawPreviewDye( MtrlTab tab, bool disabled )
{
var (dyeId, (name, dyeColor, gloss)) = _stainService.StainCombo.CurrentSelection;
var tt = dyeId == 0 ? "Select a preview dye first." : "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled.";
if( ImGuiUtil.DrawDisabledButton( "Apply Preview Dye", Vector2.Zero, tt, disabled || dyeId == 0 ) )
{
var 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 )
private static unsafe bool ColorSetPasteAllClipboardButton( MtrlTab tab, int colorSetIdx )
{
if( !ImGui.Button( "Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ) ) || file.ColorSets.Length <= colorSetIdx )
if( !ImGui.Button( "Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ) ) || tab.Mtrl.ColorSets.Length <= colorSetIdx )
{
return false;
}
@ -135,14 +138,14 @@ public partial class ModEditWindow
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 >() );
@ -150,6 +153,8 @@ public partial class ModEditWindow
}
}
tab.UpdateColorSetPreview();
return true;
}
catch
@ -182,7 +187,7 @@ public partial class ModEditWindow
}
}
private static unsafe bool ColorSetPasteFromClipboardButton( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled )
private static unsafe bool ColorSetPasteFromClipboardButton( MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled )
{
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Paste.ToIconString(), ImGui.GetFrameHeight() * Vector2.One,
"Import an exported row from your clipboard onto this row.", disabled, true ) )
@ -192,20 +197,22 @@ public partial class ModEditWindow
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 )
tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ] = *( MtrlFile.ColorSet.Row* )ptr;
if( colorSetIdx < tab.Mtrl.ColorDyeSets.Length )
{
file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] = *( MtrlFile.ColorDyeSet.Row* )( ptr + MtrlFile.ColorSet.Row.Size );
tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] = *( MtrlFile.ColorDyeSet.Row* )( ptr + MtrlFile.ColorSet.Row.Size );
}
}
tab.UpdateColorSetRowPreview(rowIdx);
return true;
}
catch
@ -217,7 +224,18 @@ public partial class ModEditWindow
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 )
{
static bool FixFloat( ref float val, float current )
{
@ -226,38 +244,41 @@ 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 = 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 ) )
{
file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = tmpFloat;
ret = true;
tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = tmpFloat;
ret = true;
tab.UpdateColorSetRowPreview(rowIdx);
}
ImGuiUtil.HoverTooltip( "Specular Strength", ImGuiHoveredFlags.AllowWhenDisabled );
@ -266,19 +287,19 @@ 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();
@ -286,8 +307,9 @@ public partial class ModEditWindow
ImGui.SetNextItemWidth( floatSize );
if( ImGui.DragFloat( "##GlossStrength", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.GlossStrength ) )
{
file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].GlossStrength = tmpFloat;
ret = true;
tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].GlossStrength = tmpFloat;
ret = true;
tab.UpdateColorSetRowPreview(rowIdx);
}
ImGuiUtil.HoverTooltip( "Gloss Strength", ImGuiHoveredFlags.AllowWhenDisabled );
@ -295,7 +317,7 @@ 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();
@ -303,8 +325,9 @@ public partial class ModEditWindow
ImGui.SetNextItemWidth( intSize );
if( ImGui.InputInt( "##TileSet", ref tmpInt, 0, 0 ) && tmpInt != row.TileSet && tmpInt is >= 0 and <= ushort.MaxValue )
{
file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].TileSet = ( ushort )tmpInt;
ret = true;
tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].TileSet = ( ushort )tmpInt;
ret = true;
tab.UpdateColorSetRowPreview(rowIdx);
}
ImGuiUtil.HoverTooltip( "Tile Set", ImGuiHoveredFlags.AllowWhenDisabled );
@ -314,8 +337,9 @@ public partial class ModEditWindow
ImGui.SetNextItemWidth( floatSize );
if( ImGui.DragFloat( "##RepeatX", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialRepeat.X ) )
{
file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { X = tmpFloat };
ret = true;
tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { X = tmpFloat };
ret = true;
tab.UpdateColorSetRowPreview(rowIdx);
}
ImGuiUtil.HoverTooltip( "Repeat X", ImGuiHoveredFlags.AllowWhenDisabled );
@ -324,8 +348,9 @@ public partial class ModEditWindow
ImGui.SetNextItemWidth( floatSize );
if( ImGui.DragFloat( "##RepeatY", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialRepeat.Y ) )
{
file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat };
ret = true;
tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat };
ret = true;
tab.UpdateColorSetRowPreview(rowIdx);
}
ImGuiUtil.HoverTooltip( "Repeat Y", ImGuiHoveredFlags.AllowWhenDisabled );
@ -335,8 +360,9 @@ public partial class ModEditWindow
ImGui.SetNextItemWidth( floatSize );
if( ImGui.DragFloat( "##SkewX", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialSkew.X ) )
{
file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { X = tmpFloat };
ret = true;
tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { X = tmpFloat };
ret = true;
tab.UpdateColorSetRowPreview(rowIdx);
}
ImGuiUtil.HoverTooltip( "Skew X", ImGuiHoveredFlags.AllowWhenDisabled );
@ -346,8 +372,9 @@ public partial class ModEditWindow
ImGui.SetNextItemWidth( floatSize );
if( ImGui.DragFloat( "##SkewY", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialSkew.Y ) )
{
file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { Y = tmpFloat };
ret = true;
tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { Y = tmpFloat };
ret = true;
tab.UpdateColorSetRowPreview(rowIdx);
}
ImGuiUtil.HoverTooltip( "Skew Y", ImGuiHoveredFlags.AllowWhenDisabled );
@ -358,14 +385,15 @@ public partial class ModEditWindow
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;
ret = true;
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
{
@ -376,7 +404,7 @@ public partial class ModEditWindow
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 ) )
@ -390,7 +418,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" );

View file

@ -0,0 +1,484 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Dalamud.Game;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Penumbra.GameData.Files;
using Penumbra.Interop.ResourceTree;
using Structs = Penumbra.Interop.Structs;
namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow
{
private static unsafe Character* FindLocalPlayer(IObjectTable objects)
{
var localPlayer = objects[0];
if (localPlayer is not Dalamud.Game.ClientState.Objects.Types.Character)
return null;
return (Character*)localPlayer.Address;
}
private static unsafe Character* FindSubActor(Character* character, int subActorType)
{
if (character == null)
return null;
switch (subActorType)
{
case -1:
return character;
case 0:
return character->Mount.MountObject;
case 1:
var companion = character->Companion.CompanionObject;
if (companion == null)
return null;
return &companion->Character;
case 2:
var ornament = character->Ornament.OrnamentObject;
if (ornament == null)
return null;
return &ornament->Character;
default:
return null;
}
}
private static unsafe List<(int SubActorType, int ChildObjectIndex, int ModelSlot, int MaterialSlot)> FindMaterial(CharacterBase* drawObject, int subActorType, string materialPath)
{
static void CollectMaterials(List<(int, int, int, int)> result, int subActorType, int childObjectIndex, CharacterBase* drawObject, string materialPath)
{
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;
if (mtrlHandle == null)
continue;
var path = ResolveContext.GetResourceHandlePath((Structs.ResourceHandle*)mtrlHandle);
if (path.ToString() == materialPath)
result.Add((subActorType, childObjectIndex, i, j));
}
}
}
var result = new List<(int, int, int, int)>();
if (drawObject == null)
return result;
materialPath = materialPath.Replace('/', '\\').ToLowerInvariant();
CollectMaterials(result, subActorType, -1, drawObject, materialPath);
var firstChildObject = (CharacterBase*)drawObject->DrawObject.Object.ChildObject;
if (firstChildObject != null)
{
var childObject = firstChildObject;
var childObjectIndex = 0;
do
{
CollectMaterials(result, subActorType, childObjectIndex, childObject, materialPath);
childObject = (CharacterBase*)childObject->DrawObject.Object.NextSiblingObject;
++childObjectIndex;
}
while (childObject != null && childObject != firstChildObject);
}
return result;
}
private static unsafe CharacterBase* GetChildObject(CharacterBase* drawObject, int index)
{
if (drawObject == null)
return null;
if (index >= 0)
{
drawObject = (CharacterBase*)drawObject->DrawObject.Object.ChildObject;
if (drawObject == null)
return null;
}
var first = drawObject;
while (index-- > 0)
{
drawObject = (CharacterBase*)drawObject->DrawObject.Object.NextSiblingObject;
if (drawObject == null || drawObject == first)
return null;
}
return drawObject;
}
private static unsafe Material* GetDrawObjectMaterial(CharacterBase* drawObject, int modelSlot, int materialSlot)
{
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];
}
private abstract unsafe class LiveMaterialPreviewerBase : IDisposable
{
private readonly IObjectTable _objects;
protected readonly int SubActorType;
protected readonly int ChildObjectIndex;
protected readonly int ModelSlot;
protected readonly int MaterialSlot;
protected readonly CharacterBase* DrawObject;
protected readonly Material* Material;
protected bool Valid;
public LiveMaterialPreviewerBase(IObjectTable objects, int subActorType, int childObjectIndex, int modelSlot, int materialSlot)
{
_objects = objects;
SubActorType = subActorType;
ChildObjectIndex = childObjectIndex;
ModelSlot = modelSlot;
MaterialSlot = materialSlot;
var localPlayer = FindLocalPlayer(objects);
if (localPlayer == null)
throw new InvalidOperationException("Cannot retrieve local player object");
var subActor = FindSubActor(localPlayer, subActorType);
if (subActor == null)
throw new InvalidOperationException("Cannot retrieve sub-actor (mount, companion or ornament)");
DrawObject = GetChildObject((CharacterBase*)subActor->GameObject.GetDrawObject(), childObjectIndex);
if (DrawObject == null)
throw new InvalidOperationException("Cannot retrieve draw object");
Material = GetDrawObjectMaterial(DrawObject, modelSlot, materialSlot);
if (Material == null)
throw new InvalidOperationException("Cannot retrieve material");
Valid = true;
}
~LiveMaterialPreviewerBase()
{
if (Valid)
Dispose(false, IsStillValid());
}
public void Dispose()
{
if (Valid)
Dispose(true, IsStillValid());
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing, bool reset)
{
Valid = false;
}
public bool CheckValidity()
{
if (Valid && !IsStillValid())
Dispose(false, false);
return Valid;
}
protected virtual bool IsStillValid()
{
var localPlayer = FindLocalPlayer(_objects);
if (localPlayer == null)
return false;
var subActor = FindSubActor(localPlayer, SubActorType);
if (subActor == null)
return false;
if (DrawObject != GetChildObject((CharacterBase*)subActor->GameObject.GetDrawObject(), ChildObjectIndex))
return false;
if (Material != GetDrawObjectMaterial(DrawObject, ModelSlot, MaterialSlot))
return false;
return true;
}
}
private 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, int subActorType, int childObjectIndex, int modelSlot, int materialSlot) : base(objects, subActorType, childObjectIndex, modelSlot, materialSlot)
{
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 Dispose(bool disposing, bool reset)
{
base.Dispose(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 cbuffer = ((Structs.Material*)Material)->MaterialParameter;
if (cbuffer == null)
return;
if (!cbuffer->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;
}
}
private 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, int subActorType, int childObjectIndex, int modelSlot, int materialSlot) : base(objects, subActorType, childObjectIndex, modelSlot, materialSlot)
{
_framework = framework;
var mtrlHandle = Material->MaterialResourceHandle;
if (mtrlHandle == null)
throw new InvalidOperationException("Material doesn't have a resource handle");
var colorSetTextures = *(Texture***)((nint)DrawObject + 0x258);
if (colorSetTextures == null)
throw new InvalidOperationException("Draw object doesn't have color set textures");
_colorSetTexture = colorSetTextures + (modelSlot * 4 + 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 Dispose(bool disposing, bool reset)
{
_framework.Update -= OnFrameworkUpdate;
base.Dispose(disposing, reset);
if (reset)
{
var oldTexture = (Texture*)Interlocked.Exchange(ref *(nint*)_colorSetTexture, (nint)_originalColorSetTexture);
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);
Structs.TextureUtility.DecRef(oldTexture);
}
else
Structs.TextureUtility.DecRef(newTexture);
}
protected override bool IsStillValid()
{
if (!base.IsStillValid())
return false;
var colorSetTextures = *(Texture***)((nint)DrawObject + 0x258);
if (colorSetTextures == null)
return false;
if (_colorSetTexture != colorSetTextures + (ModelSlot * 4 + MaterialSlot))
return false;
return true;
}
}
}

View file

@ -2,15 +2,20 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using Dalamud.Interface;
using Dalamud.Interface.Internal.Notifications;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using ImGuiNET;
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.Services;
using Penumbra.String.Classes;
using Penumbra.Util;
using static Penumbra.GameData.Files.ShpkFile;
@ -19,10 +24,12 @@ namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow
{
private sealed class MtrlTab : IWritable
private sealed class MtrlTab : IWritable, IDisposable
{
private readonly ModEditWindow _edit;
public readonly MtrlFile Mtrl;
public readonly string FilePath;
public readonly bool Writable;
public uint NewKeyId;
public uint NewKeyDefault;
@ -57,11 +64,17 @@ public partial class ModEditWindow
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;
public readonly List< (string Label, string FileName, uint Id) > 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 int HighlightTime = -1;
public FullPath FindAssociatedShpk( out string defaultPath, out Utf8GamePath defaultGamePath )
{
@ -243,7 +256,7 @@ public partial class ModEditWindow
? $"#{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 ) );
Samplers.Add( ( label, fileName, sampler.SamplerId ) );
}
MissingSamplers.Clear();
@ -269,6 +282,220 @@ public partial class ModEditWindow
}
}
public unsafe void BindToMaterialInstances()
{
UnbindFromMaterialInstances();
var localPlayer = FindLocalPlayer(_edit._dalamud.Objects);
if (null == localPlayer)
return;
var drawObject = (CharacterBase*)localPlayer->GameObject.GetDrawObject();
if (null == drawObject)
return;
var instances = FindMaterial(drawObject, -1, FilePath);
var drawObjects = stackalloc CharacterBase*[4];
drawObjects[0] = drawObject;
for (var i = 0; i < 3; ++i)
{
var subActor = FindSubActor(localPlayer, i);
if (null == subActor)
continue;
var subDrawObject = (CharacterBase*)subActor->GameObject.GetDrawObject();
if (null == subDrawObject)
continue;
instances.AddRange(FindMaterial(subDrawObject, i, FilePath));
drawObjects[i + 1] = subDrawObject;
}
var foundMaterials = new HashSet<nint>();
foreach (var (subActorType, childObjectIndex, modelSlot, materialSlot) in instances)
{
var material = GetDrawObjectMaterial(drawObjects[subActorType + 1], modelSlot, materialSlot);
if (foundMaterials.Contains((nint)material))
continue;
try
{
MaterialPreviewers.Add(new LiveMaterialPreviewer(_edit._dalamud.Objects, subActorType, childObjectIndex, modelSlot, materialSlot));
foundMaterials.Add((nint)material);
}
catch (InvalidOperationException)
{
// Carry on without that previewer.
}
}
var colorSet = Mtrl.ColorSets.FirstOrNull(colorSet => colorSet.HasRows);
if (colorSet.HasValue)
{
foreach (var (subActorType, childObjectIndex, modelSlot, materialSlot) in instances)
{
try
{
ColorSetPreviewers.Add(new LiveColorSetPreviewer(_edit._dalamud.Objects, _edit._dalamud.Framework, subActorType, childObjectIndex, modelSlot, materialSlot));
}
catch (InvalidOperationException)
{
// Carry on without that previewer.
}
}
UpdateColorSetPreview();
}
}
public void UnbindFromMaterialInstances()
{
foreach (var previewer in MaterialPreviewers)
previewer.Dispose();
MaterialPreviewers.Clear();
foreach (var previewer in ColorSetPreviewers)
previewer.Dispose();
ColorSetPreviewers.Clear();
}
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);
}
public void HighlightColorSetRow(int rowIdx)
{
var oldRowIdx = HighlightedColorSetRow;
HighlightedColorSetRow = rowIdx;
HighlightTime = (HighlightTime + 1) % 32;
if (oldRowIdx >= 0)
UpdateColorSetRowPreview(oldRowIdx);
if (rowIdx >= 0)
UpdateColorSetRowPreview(rowIdx);
}
public void CancelColorSetHighlight()
{
var rowIdx = HighlightedColorSetRow;
HighlightedColorSetRow = -1;
HighlightTime = -1;
if (rowIdx >= 0)
UpdateColorSetRowPreview(rowIdx);
}
public unsafe 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)
{
var stm = _edit._stainService.StmFile;
var dye = maybeColorDyeSet.Value.Rows[rowIdx];
if (stm.TryGetValue(dye.Template, (StainId)_edit._stainService.StainCombo.CurrentSelection.Key, out var dyes))
ApplyDye(ref row, dye, dyes);
}
if (HighlightedColorSetRow == rowIdx)
ApplyHighlight(ref row, HighlightTime);
foreach (var previewer in ColorSetPreviewers)
{
fixed (Half* pDest = previewer.ColorSet)
Buffer.MemoryCopy(&row, pDest + LiveColorSetPreviewer.TextureWidth * 4 * rowIdx, LiveColorSetPreviewer.TextureWidth * 4 * sizeof(Half), sizeof(MtrlFile.ColorSet.Row));
previewer.ScheduleUpdate();
}
}
public unsafe 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)
{
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))
ApplyDye(ref row, dye, dyes);
}
}
if (HighlightedColorSetRow >= 0)
ApplyHighlight(ref rows[HighlightedColorSetRow], HighlightTime);
foreach (var previewer in ColorSetPreviewers)
{
fixed (Half* pDest = previewer.ColorSet)
Buffer.MemoryCopy(&rows, pDest, LiveColorSetPreviewer.TextureLength * sizeof(Half), sizeof(MtrlFile.ColorSet.RowArray));
previewer.ScheduleUpdate();
}
}
private static void ApplyDye(ref MtrlFile.ColorSet.Row row, MtrlFile.ColorDyeSet.Row dye, StmFile.DyePack dyes)
{
if (dye.Diffuse)
row.Diffuse = dyes.Diffuse;
if (dye.Specular)
row.Specular = dyes.Specular;
if (dye.SpecularStrength)
row.SpecularStrength = dyes.SpecularPower;
if (dye.Emissive)
row.Emissive = dyes.Emissive;
if (dye.Gloss)
row.GlossStrength = dyes.Gloss;
}
private static void ApplyHighlight(ref MtrlFile.ColorSet.Row row, int time)
{
var level = Math.Sin(time * Math.PI / 16) * 0.5 + 0.5;
var levelSq = (float)(level * level);
row.Diffuse = Vector3.Zero;
row.Specular = Vector3.Zero;
row.Emissive = new Vector3(levelSq);
}
public void Update()
{
UpdateTextureLabels();
@ -277,11 +504,31 @@ public partial class ModEditWindow
UpdateSamplers();
}
public MtrlTab( ModEditWindow edit, MtrlFile file )
public MtrlTab( ModEditWindow edit, MtrlFile file, string filePath, bool writable )
{
_edit = edit;
Mtrl = file;
_edit = edit;
Mtrl = file;
FilePath = filePath;
Writable = writable;
LoadShpk( FindAssociatedShpk( out _, out _ ) );
if (writable)
BindToMaterialInstances();
}
~MtrlTab()
{
DoDispose();
}
public void Dispose()
{
DoDispose();
GC.SuppressFinalize(this);
}
private void DoDispose()
{
UnbindFromMaterialInstances();
}
public bool Valid

View file

@ -37,16 +37,17 @@ public partial class ModEditWindow
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;
var shpkFlags = (int)tab.Mtrl.ShaderPackage.Flags;
ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f);
if (ImGui.InputInt("Shader Package Flags", ref shpkFlags, 0, 0,
ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)))
{
file.ShaderPackage.Flags = (uint)shpkFlags;
ret = true;
tab.Mtrl.ShaderPackage.Flags = (uint)shpkFlags;
ret = true;
tab.SetShaderPackageFlags((uint)shpkFlags);
}
return ret;
@ -221,6 +222,7 @@ public partial class ModEditWindow
{
ret = true;
tab.UpdateConstantLabels();
tab.SetMaterialParameter(constant.Id, valueIdx, values.Slice(valueIdx, 1));
}
}
}
@ -247,6 +249,7 @@ public partial class ModEditWindow
ret = true;
tab.UpdateConstantLabels();
tab.SetMaterialParameter(constant.Id, 0, new float[constant.ByteSize >> 2]);
}
return ret;
@ -336,7 +339,7 @@ public partial class ModEditWindow
private static bool DrawMaterialSampler(MtrlTab tab, bool disabled, ref int idx)
{
var (label, filename) = tab.Samplers[idx];
var (label, filename, samplerCrc) = tab.Samplers[idx];
using var tree = ImRaii.TreeNode(label);
if (!tree)
return false;
@ -366,6 +369,7 @@ public partial class ModEditWindow
{
tab.Mtrl.ShaderPackage.Samplers[idx].Flags = (uint)samplerFlags;
ret = true;
tab.SetSamplerFlags(samplerCrc, (uint)samplerFlags);
}
if (!disabled
@ -410,9 +414,10 @@ public partial class ModEditWindow
if (!ImGui.Button("Add Sampler"))
return false;
var newSamplerId = tab.NewSamplerId;
tab.Mtrl.ShaderPackage.Samplers = tab.Mtrl.ShaderPackage.Samplers.AddItem(new Sampler
{
SamplerId = tab.NewSamplerId,
SamplerId = newSamplerId,
TextureIndex = (byte)tab.Mtrl.Textures.Length,
Flags = 0,
});
@ -423,6 +428,7 @@ public partial class ModEditWindow
});
tab.UpdateSamplers();
tab.UpdateTextureLabels();
tab.SetSamplerFlags(newSamplerId, 0);
return true;
}
@ -467,7 +473,7 @@ public partial class ModEditWindow
return ret;
ret |= DrawPackageNameInput(tab, disabled);
ret |= DrawShaderFlagsInput(tab.Mtrl, disabled);
ret |= DrawShaderFlagsInput(tab, disabled);
DrawCustomAssociations(tab);
ret |= DrawMaterialShaderKeys(tab, disabled);
DrawMaterialShaders(tab);

View file

@ -15,13 +15,16 @@ public partial class ModEditWindow
private bool DrawMaterialPanel( MtrlTab tab, bool disabled )
{
DrawMaterialLivePreviewRebind( tab, disabled );
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
var ret = DrawMaterialTextureChange( tab, disabled );
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
ret |= DrawBackFaceAndTransparency( tab.Mtrl, disabled );
ret |= DrawBackFaceAndTransparency( tab, disabled );
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
ret |= DrawMaterialColorSetChange( tab.Mtrl, disabled );
ret |= DrawMaterialColorSetChange( tab, disabled );
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
ret |= DrawMaterialShaderResources( tab, disabled );
@ -32,6 +35,15 @@ 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();
}
private static bool DrawMaterialTextureChange( MtrlTab tab, bool disabled )
{
var ret = false;
@ -62,7 +74,7 @@ public partial class ModEditWindow
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 +83,21 @@ 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;
ret = true;
tab.Mtrl.ShaderPackage.Flags = tmp ? tab.Mtrl.ShaderPackage.Flags | transparencyBit : tab.Mtrl.ShaderPackage.Flags & ~transparencyBit;
ret = true;
tab.SetShaderPackageFlags(tab.Mtrl.ShaderPackage.Flags);
}
ImGui.SameLine( 200 * UiHelpers.Scale + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X );
tmp = ( file.ShaderPackage.Flags & backfaceBit ) != 0;
tmp = ( tab.Mtrl.ShaderPackage.Flags & backfaceBit ) != 0;
if( ImGui.Checkbox( "Hide Backfaces", ref tmp ) )
{
file.ShaderPackage.Flags = tmp ? file.ShaderPackage.Flags | backfaceBit : file.ShaderPackage.Flags & ~backfaceBit;
ret = true;
tab.Mtrl.ShaderPackage.Flags = tmp ? tab.Mtrl.ShaderPackage.Flags | backfaceBit : tab.Mtrl.ShaderPackage.Flags & ~backfaceBit;
ret = true;
tab.SetShaderPackageFlags(tab.Mtrl.ShaderPackage.Flags);
}
return ret;

View file

@ -137,6 +137,9 @@ public partial class ModEditWindow : Window, IDisposable
{
_left.Dispose();
_right.Dispose();
_materialTab.Reset();
_modelTab.Reset();
_shaderPackageTab.Reset();
}
public override void Draw()
@ -541,12 +544,12 @@ public partial class ModEditWindow : Window, IDisposable
_fileDialog = fileDialog;
_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 +560,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();