Material editor 2099

This commit is contained in:
Exter-N 2023-08-24 05:51:21 +02:00
parent f64fdd2b26
commit b8d09ab660
15 changed files with 1221 additions and 651 deletions

@ -1 +1 @@
Subproject commit 863d08bd83381bb7fe4a8d5c514f0ba55379336f Subproject commit 1e172ee9a0f5946d67b848a36b2be97f6541453f

@ -1 +1 @@
Subproject commit 97643cad67b6981c3ee510d1ca12c4321e6a80bf Subproject commit 07c001c5b2b35b2dba2b8428389d3ed375728616

View file

@ -190,7 +190,7 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
if (WithNames) 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}")); node.Children.Add(texNode.WithName(name ?? $"Texture #{i}"));
} }
else else

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

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

View file

@ -13,22 +13,44 @@ namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow public partial class ModEditWindow
{ {
private static readonly float HalfMinValue = (float)Half.MinValue;
private static readonly float HalfMaxValue = (float)Half.MaxValue;
private static readonly float HalfEpsilon = (float)Half.Epsilon;
private bool DrawMaterialColorSetChange( MtrlTab tab, bool disabled ) private bool DrawMaterialColorSetChange( MtrlTab tab, bool disabled )
{ {
if( !tab.Mtrl.ColorSets.Any( c => c.HasRows ) ) if( !tab.SamplerIds.Contains( ShpkFile.TableSamplerId ) || !tab.Mtrl.ColorSets.Any( c => c.HasRows ) )
{ {
return false; 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 ); ColorSetCopyAllClipboardButton( tab.Mtrl, 0 );
ImGui.SameLine(); ImGui.SameLine();
var ret = ColorSetPasteAllClipboardButton( tab, 0 ); var ret = ColorSetPasteAllClipboardButton( tab, 0, disabled );
ImGui.SameLine(); if( !disabled )
ImGui.Dummy( ImGuiHelpers.ScaledVector2( 20, 0 ) ); {
ImGui.SameLine(); ImGui.SameLine();
ret |= DrawPreviewDye( tab, disabled ); ImGui.Dummy( ImGuiHelpers.ScaledVector2( 20, 0 ) );
ImGui.SameLine();
ret |= ColorSetDyeableCheckbox( tab, ref hasAnyDye );
}
if( hasAnyDye )
{
ImGui.SameLine();
ImGui.Dummy( ImGuiHelpers.ScaledVector2( 20, 0 ) );
ImGui.SameLine();
ret |= DrawPreviewDye( tab, disabled );
}
using var table = ImRaii.Table( "##ColorSets", 11, using var table = ImRaii.Table( "##ColorSets", hasAnyDye ? 11 : 9,
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV ); ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV );
if( !table ) if( !table )
{ {
@ -53,17 +75,20 @@ public partial class ModEditWindow
ImGui.TableHeader( "Repeat" ); ImGui.TableHeader( "Repeat" );
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.TableHeader( "Skew" ); ImGui.TableHeader( "Skew" );
ImGui.TableNextColumn(); if( hasAnyDye )
ImGui.TableHeader( "Dye" ); {
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.TableHeader( "Dye Preview" ); ImGui.TableHeader("Dye");
ImGui.TableNextColumn();
ImGui.TableHeader("Dye Preview");
}
for( var j = 0; j < tab.Mtrl.ColorSets.Length; ++j ) for( var j = 0; j < tab.Mtrl.ColorSets.Length; ++j )
{ {
using var _ = ImRaii.PushId( j ); using var _ = ImRaii.PushId( j );
for( var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i ) for( var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i )
{ {
ret |= DrawColorSetRow( tab, j, i, disabled ); ret |= DrawColorSetRow( tab, j, i, disabled, hasAnyDye );
ImGui.TableNextRow(); ImGui.TableNextRow();
} }
} }
@ -122,9 +147,9 @@ public partial class ModEditWindow
return false; return false;
} }
private static unsafe bool ColorSetPasteAllClipboardButton( MtrlTab tab, int colorSetIdx ) private static unsafe bool ColorSetPasteAllClipboardButton( MtrlTab tab, int colorSetIdx, bool disabled )
{ {
if( !ImGui.Button( "Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ) ) || tab.Mtrl.ColorSets.Length <= colorSetIdx ) if( !ImGuiUtil.DrawDisabledButton( "Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ), string.Empty, disabled ) || tab.Mtrl.ColorSets.Length <= colorSetIdx )
{ {
return false; return false;
} }
@ -187,6 +212,21 @@ public partial class ModEditWindow
} }
} }
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();
}
return ret;
}
private static unsafe bool ColorSetPasteFromClipboardButton( MtrlTab tab, 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, if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Paste.ToIconString(), ImGui.GetFrameHeight() * Vector2.One,
@ -235,7 +275,7 @@ public partial class ModEditWindow
tab.CancelColorSetHighlight(); tab.CancelColorSetHighlight();
} }
private bool DrawColorSetRow( MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled ) private bool DrawColorSetRow( MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled, bool hasAnyDye )
{ {
static bool FixFloat( ref float val, float current ) static bool FixFloat( ref float val, float current )
{ {
@ -245,7 +285,7 @@ public partial class ModEditWindow
using var id = ImRaii.PushId( rowIdx ); using var id = ImRaii.PushId( rowIdx );
var row = tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ]; var row = tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ];
var hasDye = tab.Mtrl.ColorDyeSets.Length > colorSetIdx; var hasDye = hasAnyDye && tab.Mtrl.ColorDyeSets.Length > colorSetIdx;
var dye = hasDye ? tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] : new MtrlFile.ColorDyeSet.Row(); var dye = hasDye ? tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] : new MtrlFile.ColorDyeSet.Row();
var floatSize = 70 * UiHelpers.Scale; var floatSize = 70 * UiHelpers.Scale;
var intSize = 45 * UiHelpers.Scale; var intSize = 45 * UiHelpers.Scale;
@ -274,7 +314,7 @@ public partial class ModEditWindow
ImGui.SameLine(); ImGui.SameLine();
var tmpFloat = row.SpecularStrength; var tmpFloat = row.SpecularStrength;
ImGui.SetNextItemWidth( floatSize ); 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 ) )
{ {
tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = tmpFloat; tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = tmpFloat;
ret = true; ret = true;
@ -305,9 +345,9 @@ public partial class ModEditWindow
ImGui.TableNextColumn(); ImGui.TableNextColumn();
tmpFloat = row.GlossStrength; tmpFloat = row.GlossStrength;
ImGui.SetNextItemWidth( floatSize ); 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 ) )
{ {
tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].GlossStrength = tmpFloat; tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].GlossStrength = Math.Max(tmpFloat, HalfEpsilon);
ret = true; ret = true;
tab.UpdateColorSetRowPreview(rowIdx); tab.UpdateColorSetRowPreview(rowIdx);
} }
@ -323,9 +363,9 @@ public partial class ModEditWindow
ImGui.TableNextColumn(); ImGui.TableNextColumn();
int tmpInt = row.TileSet; int tmpInt = row.TileSet;
ImGui.SetNextItemWidth( intSize ); 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 )
{ {
tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].TileSet = ( ushort )tmpInt; tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].TileSet = ( ushort )Math.Clamp(tmpInt, 0, 63);
ret = true; ret = true;
tab.UpdateColorSetRowPreview(rowIdx); tab.UpdateColorSetRowPreview(rowIdx);
} }
@ -335,7 +375,7 @@ public partial class ModEditWindow
ImGui.TableNextColumn(); ImGui.TableNextColumn();
tmpFloat = row.MaterialRepeat.X; tmpFloat = row.MaterialRepeat.X;
ImGui.SetNextItemWidth( floatSize ); 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 ) )
{ {
tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { X = tmpFloat }; tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { X = tmpFloat };
ret = true; ret = true;
@ -346,7 +386,7 @@ public partial class ModEditWindow
ImGui.SameLine(); ImGui.SameLine();
tmpFloat = row.MaterialRepeat.Y; tmpFloat = row.MaterialRepeat.Y;
ImGui.SetNextItemWidth( floatSize ); 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 ) )
{ {
tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat }; tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat };
ret = true; ret = true;
@ -358,7 +398,7 @@ public partial class ModEditWindow
ImGui.TableNextColumn(); ImGui.TableNextColumn();
tmpFloat = row.MaterialSkew.X; tmpFloat = row.MaterialSkew.X;
ImGui.SetNextItemWidth( floatSize ); 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 ) )
{ {
tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { X = tmpFloat }; tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { X = tmpFloat };
ret = true; ret = true;
@ -370,7 +410,7 @@ public partial class ModEditWindow
ImGui.SameLine(); ImGui.SameLine();
tmpFloat = row.MaterialSkew.Y; tmpFloat = row.MaterialSkew.Y;
ImGui.SetNextItemWidth( floatSize ); 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 ) )
{ {
tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { Y = tmpFloat }; tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { Y = tmpFloat };
ret = true; ret = true;
@ -379,10 +419,10 @@ public partial class ModEditWindow
ImGuiUtil.HoverTooltip( "Skew Y", ImGuiHoveredFlags.AllowWhenDisabled ); ImGuiUtil.HoverTooltip( "Skew Y", ImGuiHoveredFlags.AllowWhenDisabled );
ImGui.TableNextColumn();
if( hasDye ) if( hasDye )
{ {
if(_stainService.TemplateCombo.Draw( "##dyeTemplate", dye.Template.ToString(), string.Empty, intSize ImGui.TableNextColumn();
if (_stainService.TemplateCombo.Draw( "##dyeTemplate", dye.Template.ToString(), string.Empty, intSize
+ ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton ) ) + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton ) )
{ {
tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = _stainService.TemplateCombo.CurrentSelection; tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = _stainService.TemplateCombo.CurrentSelection;
@ -395,9 +435,10 @@ public partial class ModEditWindow
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ret |= DrawDyePreview( tab, colorSetIdx, rowIdx, disabled, dye, floatSize ); ret |= DrawDyePreview( tab, colorSetIdx, rowIdx, disabled, dye, floatSize );
} }
else else if ( hasAnyDye )
{ {
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.TableNextColumn();
} }
@ -431,10 +472,10 @@ public partial class ModEditWindow
ImGui.SameLine(); ImGui.SameLine();
using var dis = ImRaii.Disabled(); using var dis = ImRaii.Disabled();
ImGui.SetNextItemWidth( floatSize ); 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.SameLine();
ImGui.SetNextItemWidth( floatSize ); 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; return ret;
} }

View file

@ -0,0 +1,235 @@
using System;
using System.Collections.Generic;
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)
{
if (values.Length == 3)
{
ImGui.SetNextItemWidth(editorWidth);
var value = new Vector3(values);
if (_squaredRgb)
value = Vector3.SquareRoot(value);
if (ImGui.ColorEdit3("##0", ref value) && !disabled)
{
if (_squaredRgb)
value *= value;
if (_clamped)
value = Vector3.Clamp(value, Vector3.Zero, Vector3.One);
value.CopyTo(values);
return true;
}
return false;
}
else if (values.Length == 4)
{
ImGui.SetNextItemWidth(editorWidth);
var value = new Vector4(values);
if (_squaredRgb)
value = new Vector4(MathF.Sqrt(value.X), MathF.Sqrt(value.Y), MathF.Sqrt(value.Z), value.W);
if (ImGui.ColorEdit4("##0", ref value) && !disabled)
{
if (_squaredRgb)
value *= new Vector4(value.X, value.Y, value.Z, 1.0f);
if (_clamped)
value = Vector4.Clamp(value, Vector4.Zero, Vector4.One);
value.CopyTo(values);
return true;
}
return false;
}
else
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)
{
if (valueIdx > 0)
ImGui.SameLine();
ImGui.SetNextItemWidth(MathF.Round(fieldWidth * (valueIdx + 1)) - MathF.Round(fieldWidth * valueIdx));
var currentValue = values[valueIdx];
var (currentLabel, _, currentDescription) = _values.FirstOrNull(v => v.Value == currentValue) ?? (currentValue.ToString(), currentValue, string.Empty);
if (disabled)
ImGui.InputText($"##{valueIdx}", ref currentLabel, (uint)currentLabel.Length, ImGuiInputTextFlags.ReadOnly);
else
{
using var c = ImRaii.Combo($"##{valueIdx}", currentLabel);
{
if (c)
foreach (var (valueLabel, value, valueDescription) in _values)
{
if (ImGui.Selectable(valueLabel, value == currentValue))
{
values[valueIdx] = value;
ret = true;
}
if (valueDescription.Length > 0)
ImGuiUtil.SelectableHelpMarker(valueDescription);
}
}
}
}
return ret;
}
}
}

View file

@ -398,7 +398,7 @@ public partial class ModEditWindow
if (mtrlHandle == null) if (mtrlHandle == null)
throw new InvalidOperationException("Material doesn't have a resource handle"); throw new InvalidOperationException("Material doesn't have a resource handle");
var colorSetTextures = *(Texture***)((nint)DrawObject + 0x258); var colorSetTextures = ((Structs.CharacterBaseExt*)DrawObject)->ColorSetTextures;
if (colorSetTextures == null) if (colorSetTextures == null)
throw new InvalidOperationException("Draw object doesn't have color set textures"); throw new InvalidOperationException("Draw object doesn't have color set textures");
@ -424,7 +424,8 @@ public partial class ModEditWindow
if (reset) if (reset)
{ {
var oldTexture = (Texture*)Interlocked.Exchange(ref *(nint*)_colorSetTexture, (nint)_originalColorSetTexture); var oldTexture = (Texture*)Interlocked.Exchange(ref *(nint*)_colorSetTexture, (nint)_originalColorSetTexture);
Structs.TextureUtility.DecRef(oldTexture); if (oldTexture != null)
Structs.TextureUtility.DecRef(oldTexture);
} }
else else
Structs.TextureUtility.DecRef(_originalColorSetTexture); Structs.TextureUtility.DecRef(_originalColorSetTexture);
@ -460,7 +461,8 @@ public partial class ModEditWindow
if (success) if (success)
{ {
var oldTexture = (Texture*)Interlocked.Exchange(ref *(nint*)_colorSetTexture, (nint)newTexture); var oldTexture = (Texture*)Interlocked.Exchange(ref *(nint*)_colorSetTexture, (nint)newTexture);
Structs.TextureUtility.DecRef(oldTexture); if (oldTexture != null)
Structs.TextureUtility.DecRef(oldTexture);
} }
else else
Structs.TextureUtility.DecRef(newTexture); Structs.TextureUtility.DecRef(newTexture);
@ -471,7 +473,7 @@ public partial class ModEditWindow
if (!base.IsStillValid()) if (!base.IsStillValid())
return false; return false;
var colorSetTextures = *(Texture***)((nint)DrawObject + 0x258); var colorSetTextures = ((Structs.CharacterBaseExt*)DrawObject)->ColorSetTextures;
if (colorSetTextures == null) if (colorSetTextures == null)
return false; return false;

View file

@ -8,6 +8,7 @@ using Dalamud.Interface.Internal.Notifications;
using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using ImGuiNET; using ImGuiNET;
using Newtonsoft.Json.Linq;
using OtterGui; using OtterGui;
using OtterGui.Classes; using OtterGui.Classes;
using OtterGui.Raii; using OtterGui.Raii;
@ -16,6 +17,7 @@ using Penumbra.GameData.Data;
using Penumbra.GameData.Files; using Penumbra.GameData.Files;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.String;
using Penumbra.String.Classes; using Penumbra.String.Classes;
using Penumbra.Util; using Penumbra.Util;
using static Penumbra.GameData.Files.ShpkFile; using static Penumbra.GameData.Files.ShpkFile;
@ -26,49 +28,47 @@ public partial class ModEditWindow
{ {
private sealed class MtrlTab : IWritable, IDisposable 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; private readonly ModEditWindow _edit;
public readonly MtrlFile Mtrl; public readonly MtrlFile Mtrl;
public readonly string FilePath; public readonly string FilePath;
public readonly bool Writable; public readonly bool Writable;
public uint NewKeyId; private string[]? _shpkNames;
public uint NewKeyDefault;
public uint NewConstantId;
public int NewConstantIdx;
public uint NewSamplerId;
public int NewSamplerIdx;
public string ShaderHeader = "Shader###Shader";
public FullPath LoadedShpkPath = FullPath.Empty;
public string LoadedShpkPathName = string.Empty;
public string LoadedShpkDevkitPathName = string.Empty;
public string ShaderComment = string.Empty;
public ShpkFile? AssociatedShpk;
public JObject? AssociatedShpkDevkit;
public ShpkFile? AssociatedShpk; public readonly string LoadedBaseDevkitPathName = string.Empty;
public readonly List< string > TextureLabels = new(4); public readonly JObject? AssociatedBaseDevkit;
public FullPath LoadedShpkPath = FullPath.Empty;
public string LoadedShpkPathName = string.Empty;
public float TextureLabelWidth;
// Shader Key State // Shader Key State
public readonly List< string > ShaderKeyLabels = new(16); public readonly List< (string Label, int Index, string Description, bool MonoFont, IReadOnlyList< (string Label, uint Value, string Description) > Values) > ShaderKeys = new(16);
public readonly Dictionary< uint, uint > DefinedShaderKeys = new(16);
public readonly List< int > MissingShaderKeyIndices = new(16); public readonly HashSet< int > VertexShaders = new(16);
public readonly List< uint > AvailableKeyValues = new(16); public readonly HashSet< int > PixelShaders = new(16);
public string VertexShaders = "Vertex Shaders: ???"; public bool ShadersKnown = false;
public string PixelShaders = "Pixel Shaders: ???"; 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 // Material Constants
public readonly List< (string Name, bool ComponentOnly, int ParamValueOffset) > MaterialConstants = 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 readonly List< (string Name, uint Id, ushort ByteSize) > MissingMaterialConstants = new(16);
public readonly HashSet< uint > DefinedMaterialConstants = 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, 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 // Live-Previewers
public readonly List<LiveMaterialPreviewer> MaterialPreviewers = new(4); public readonly List<LiveMaterialPreviewer> MaterialPreviewers = new(4);
@ -87,8 +87,24 @@ public partial class ModEditWindow
return _edit.FindBestMatch( defaultGamePath ); 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 ) public void LoadShpk( FullPath path )
{ {
ShaderHeader = $"Shader ({Mtrl.ShaderPackage.Name})###Shader";
try try
{ {
LoadedShpkPath = path; LoadedShpkPath = path;
@ -106,180 +122,314 @@ public partial class ModEditWindow
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(); Update();
} }
public void UpdateTextureLabels() private JObject? TryLoadShpkDevkit(string shpkBaseName, out string devkitPathName)
{ {
var samplers = Mtrl.GetSamplersByTexture( AssociatedShpk ); try
TextureLabels.Clear();
TextureLabelWidth = 50f * UiHelpers.Scale;
using( var _ = ImRaii.PushFont( UiBuilder.MonoFont ) )
{ {
for( var i = 0; i < Mtrl.Textures.Length; ++i ) if (!Utf8GamePath.FromString("penumbra/shpk_devkit/" + shpkBaseName + ".json", out var devkitPath))
{ throw new Exception("Could not assemble ShPk dev-kit path.");
var (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 );
}
}
TextureLabelWidth = TextureLabelWidth / UiHelpers.Scale + 4; 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;
}
} }
public void UpdateShaderKeyLabels() private T? TryGetShpkDevkitData<T>(string category, uint? id, bool mayVary) where T : class
{ {
ShaderKeyLabels.Clear(); return TryGetShpkDevkitData<T>(AssociatedShpkDevkit, LoadedShpkDevkitPathName, category, id, mayVary)
DefinedShaderKeys.Clear(); ?? TryGetShpkDevkitData<T>(AssociatedBaseDevkit, LoadedBaseDevkitPathName, category, id, mayVary);
foreach( var (key, idx) in Mtrl.ShaderPackage.ShaderKeys.WithIndex() ) }
{
ShaderKeyLabels.Add( $"#{idx}: 0x{key.Category:X8} = 0x{key.Value:X8}###{idx}: 0x{key.Category:X8}" );
DefinedShaderKeys.Add( key.Category, key.Value );
}
MissingShaderKeyIndices.Clear(); private T? TryGetShpkDevkitData<T>(JObject? devkit, string devkitPathName, string category, uint? id, bool mayVary) where T : class
AvailableKeyValues.Clear(); {
var vertexShaders = new IndexSet( AssociatedShpk?.VertexShaders.Length ?? 0, false ); if (devkit == null)
var pixelShaders = new IndexSet( AssociatedShpk?.PixelShaders.Length ?? 0, false ); return null;
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 ) ) try
{
var data = devkit[category];
if (id.HasValue)
data = data?[id.Value.ToString()];
if (mayVary && (data as JObject)?["Vary"] != null)
{ {
var key = AssociatedShpk.MaterialKeys[ MissingShaderKeyIndices[ 0 ] ]; var selector = BuildSelector(data!["Vary"]!
NewKeyId = key.Id; .Select(key => (uint)key)
NewKeyDefault = key.DefaultValue; .Select(key => Mtrl.GetShaderKey(key)?.Value ?? AssociatedShpk!.GetMaterialKeyById(key)!.Value.DefaultValue));
var index = (int)data["Selectors"]![selector.ToString()]!;
data = data["Items"]![index];
} }
AvailableKeyValues.AddRange( AssociatedShpk.MaterialKeys.Select( k => DefinedShaderKeys.TryGetValue( k.Id, out var value ) ? value : k.DefaultValue ) ); return data?.ToObject(typeof(T)) as T;
foreach( var node in AssociatedShpk.Nodes ) }
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;
}
}
public void UpdateShaderKeys()
{
ShaderKeys.Clear();
if (AssociatedShpk != null)
{
foreach (var key in AssociatedShpk.MaterialKeys)
{ {
if( node.MaterialKeys.WithIndex().All( key => key.Value == AvailableKeyValues[ key.Index ] ) ) 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 =>
{ {
foreach( var pass in node.Passes ) if (dkData != null && dkData.Values.TryGetValue(value, out var dkValue))
return (dkValue.Label.Length > 0 ? dkValue.Label : $"0x{value:X8}", value, dkValue.Description);
else
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 x.Label.CompareTo(y.Label);
});
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)>()));
}
}
public 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)
{ {
vertexShaders.Add( ( int )pass.VertexShader ); var selector = BuildSelector(systemKeySelector, sceneKeySelector, materialKeySelector, subViewKeySelector);
pixelShaders.Add( ( int )pass.PixelShader ); 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;
} }
} }
} }
} }
VertexShaders = $"Vertex Shaders: {( vertexShaders.Count > 0 ? string.Join( ", ", vertexShaders.Select( i => $"#{i}" ) ) : "???" )}"; var vertexShaders = VertexShaders.OrderBy(i => i).Select(i => $"#{i}");
PixelShaders = $"Pixel Shaders: {( pixelShaders.Count > 0 ? string.Join( ", ", pixelShaders.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;
} }
public void UpdateConstantLabels() public void UpdateTextures()
{ {
var prefix = AssociatedShpk?.GetConstantById( MaterialParamsConstantId )?.Name ?? string.Empty; Textures.Clear();
MaterialConstantLabel = prefix.Length == 0 ? "Constants###Constants" : prefix + "###Constants"; SamplerIds.Clear();
if (AssociatedShpk == null)
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 ); SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId));
var values = Mtrl.GetConstantValues( constant ); if (Mtrl.ColorSets.Any(c => c.HasRows))
var paramValueOffset = -values.Length; SamplerIds.Add(TableSamplerId);
if( values.Length > 0 )
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)
{ {
var shpkParam = AssociatedShpk?.GetMaterialParamById( constant.Id ); SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId));
var paramByteOffset = shpkParam?.ByteOffset ?? -1; if (Mtrl.ColorSets.Any(c => c.HasRows))
if( ( paramByteOffset & 0x3 ) == 0 ) SamplerIds.Add(TableSamplerId);
{
paramValueOffset = paramByteOffset >> 2;
}
var unique = OrphanedMaterialValues.RemoveRange( constant.ByteOffset >> 2, values.Length );
AliasedMaterialValueCount += values.Length - unique;
} }
else foreach (var samplerId in SamplerIds)
{ {
HasMalformedMaterialConstants = true; var shpkSampler = AssociatedShpk.GetSamplerById(samplerId);
if (!shpkSampler.HasValue || shpkSampler.Value.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));
var (name, componentOnly) = MaterialParamRangeName( prefix, paramValueOffset, values.Length ); TextureLabelWidth = 50f * UiHelpers.Scale;
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 ) ); float helpWidth;
using (var _ = ImRaii.PushFont(UiBuilder.IconFont))
helpWidth = ImGui.GetStyle().ItemSpacing.X + ImGui.CalcTextSize(FontAwesomeIcon.InfoCircle.ToIconString()).X;
foreach (var (label, _, _, description, monoFont) in Textures)
if (!monoFont)
TextureLabelWidth = Math.Max(TextureLabelWidth, ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f));
using (var _ = ImRaii.PushFont(UiBuilder.MonoFont))
{
foreach (var (label, _, _, description, monoFont) in Textures)
if (monoFont)
TextureLabelWidth = Math.Max(TextureLabelWidth, ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f));
} }
MissingMaterialConstants.Clear(); TextureLabelWidth = TextureLabelWidth / UiHelpers.Scale + 4;
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 )
{
NewConstantIdx = 0;
NewConstantId = MissingMaterialConstants[ 0 ].Id;
}
}
} }
public void UpdateSamplers() public void UpdateConstants()
{ {
Samplers.Clear(); static List<T> FindOrAddGroup<T>(List<(string, List<T>)> groups, string name)
DefinedSamplers.Clear();
OrphanedSamplers = new IndexSet( Mtrl.Textures.Length, true );
foreach( var (sampler, idx) in Mtrl.ShaderPackage.Samplers.WithIndex() )
{ {
DefinedSamplers.Add( sampler.SamplerId ); foreach (var (groupName, group) in groups)
if( !OrphanedSamplers.Remove( sampler.TextureIndex ) ) if (string.Equals(name, groupName, StringComparison.Ordinal))
{ return group;
++AliasedSamplerCount;
}
var shpk = AssociatedShpk?.GetSamplerById( sampler.SamplerId ); var newGroup = new List<T>(16);
var label = shpk.HasValue groups.Add((name, newGroup));
? $"#{idx}: {shpk.Value.Name} (ID: 0x{sampler.SamplerId:X8})##{sampler.SamplerId}" return newGroup;
: $"#{idx} (ID: 0x{sampler.SamplerId:X8})##{sampler.SamplerId}";
var fileName = $"Texture #{sampler.TextureIndex} - {Path.GetFileName( Mtrl.Textures[ sampler.TextureIndex ].Path )}";
Samplers.Add( ( label, fileName, sampler.SamplerId ) );
} }
MissingSamplers.Clear(); Constants.Clear();
if( AssociatedShpk != null ) if (AssociatedShpk == null)
{ {
var setSampler = false; var fcGroup = FindOrAddGroup(Constants, "Further Constants");
foreach( var sampler in AssociatedShpk.Samplers.Where( s => s.Slot == 2 && !DefinedSamplers.Contains( s.Id ) ) ) foreach (var (constant, index) in Mtrl.ShaderPackage.Constants.WithIndex())
{ {
if( sampler.Id == NewSamplerId ) var values = Mtrl.GetConstantValues(constant);
for (var i = 0; i < values.Length; i += 4)
fcGroup.Add(($"0x{constant.Id:X8}", index, i..Math.Min(i + 4, values.Length), string.Empty, true, FloatConstantEditor.Default));
}
}
else
{
var prefix = AssociatedShpk.GetConstantById(MaterialParamsConstantId)?.Name ?? string.Empty;
foreach (var shpkConstant in AssociatedShpk.MaterialParams)
{
if ((shpkConstant.ByteSize & 0x3) != 0)
continue;
var constant = Mtrl.GetOrAddConstant(shpkConstant.Id, shpkConstant.ByteSize >> 2, out var constantIndex);
var values = Mtrl.GetConstantValues(constant);
var handledElements = new IndexSet(values.Length, false);
var dkData = TryGetShpkDevkitData<DevkitConstant[]>("Constants", shpkConstant.Id, true);
if (dkData != null)
{ {
setSampler = true; foreach (var dkConstant in dkData)
NewSamplerIdx = MissingSamplers.Count; {
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);
}
} }
MissingSamplers.Add( ( sampler.Name, sampler.Id ) ); var fcGroup = FindOrAddGroup(Constants, "Further Constants");
} foreach (var (start, end) in handledElements.Ranges(true))
{
if( !setSampler && MissingSamplers.Count > 0 ) if ((shpkConstant.ByteOffset & 0x3) == 0)
{ {
NewSamplerIdx = 0; var offset = shpkConstant.ByteOffset >> 2;
NewSamplerId = MissingSamplers[ 0 ].Id; for (int i = (start & ~0x3) - (offset & 0x3), j = offset >> 2; i < end; i += 4, ++j)
{
var rangeStart = Math.Max(i, start);
var rangeEnd = Math.Min(i + 4, end);
if (rangeEnd > rangeStart)
fcGroup.Add(($"{prefix}[{j:D2}]{VectorSwizzle((offset + rangeStart) & 0x3, (offset + rangeEnd - 1) & 0x3)} (0x{shpkConstant.Id:X8})", constantIndex, rangeStart..rangeEnd, string.Empty, true, FloatConstantEditor.Default));
}
}
else
{
for (var i = start; i < end; i += 4)
fcGroup.Add(($"0x{shpkConstant.Id:X8}", constantIndex, i..Math.Min(i + 4, end), string.Empty, true, FloatConstantEditor.Default));
}
}
} }
} }
Constants.RemoveAll(group => group.Constants.Count == 0);
Constants.Sort((x, y) =>
{
if (string.Equals(x.Header, "Further Constants", StringComparison.Ordinal))
return 1;
if (string.Equals(y.Header, "Further Constants", StringComparison.Ordinal))
return -1;
return string.Compare(x.Header, y.Header, StringComparison.Ordinal);
});
// HACK the Replace makes w appear after xyz, for the cbuffer-location-based naming scheme
foreach (var (_, group) in Constants)
group.Sort((x, y) => string.CompareOrdinal(
x.MonoFont ? x.Label.Replace("].w", "].{") : x.Label,
y.MonoFont ? y.Label.Replace("].w", "].{") : y.Label));
} }
public unsafe void BindToMaterialInstances() public unsafe void BindToMaterialInstances()
@ -329,6 +479,7 @@ public partial class ModEditWindow
// Carry on without that previewer. // Carry on without that previewer.
} }
} }
UpdateMaterialPreview();
var colorSet = Mtrl.ColorSets.FirstOrNull(colorSet => colorSet.HasRows); var colorSet = Mtrl.ColorSets.FirstOrNull(colorSet => colorSet.HasRows);
@ -378,6 +529,19 @@ public partial class ModEditWindow
previewer.SetSamplerFlags(samplerCrc, samplerFlags); previewer.SetSamplerFlags(samplerCrc, samplerFlags);
} }
public 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) public void HighlightColorSetRow(int rowIdx)
{ {
var oldRowIdx = HighlightedColorSetRow; var oldRowIdx = HighlightedColorSetRow;
@ -402,7 +566,7 @@ public partial class ModEditWindow
UpdateColorSetRowPreview(rowIdx); UpdateColorSetRowPreview(rowIdx);
} }
public unsafe void UpdateColorSetRowPreview(int rowIdx) public void UpdateColorSetRowPreview(int rowIdx)
{ {
if (ColorSetPreviewers.Count == 0) if (ColorSetPreviewers.Count == 0)
return; return;
@ -415,12 +579,12 @@ public partial class ModEditWindow
var maybeColorDyeSet = Mtrl.ColorDyeSets.FirstOrNull(colorDyeSet => colorDyeSet.Index == colorSet.Index); var maybeColorDyeSet = Mtrl.ColorDyeSets.FirstOrNull(colorDyeSet => colorDyeSet.Index == colorSet.Index);
var row = colorSet.Rows[rowIdx]; var row = colorSet.Rows[rowIdx];
if (maybeColorDyeSet.HasValue) if (maybeColorDyeSet.HasValue && UseColorDyeSet)
{ {
var stm = _edit._stainService.StmFile; var stm = _edit._stainService.StmFile;
var dye = maybeColorDyeSet.Value.Rows[rowIdx]; var dye = maybeColorDyeSet.Value.Rows[rowIdx];
if (stm.TryGetValue(dye.Template, (StainId)_edit._stainService.StainCombo.CurrentSelection.Key, out var dyes)) if (stm.TryGetValue(dye.Template, (StainId)_edit._stainService.StainCombo.CurrentSelection.Key, out var dyes))
ApplyDye(ref row, dye, dyes); row.ApplyDyeTemplate(dye, dyes);
} }
if (HighlightedColorSetRow == rowIdx) if (HighlightedColorSetRow == rowIdx)
@ -428,13 +592,12 @@ public partial class ModEditWindow
foreach (var previewer in ColorSetPreviewers) foreach (var previewer in ColorSetPreviewers)
{ {
fixed (Half* pDest = previewer.ColorSet) row.AsHalves().CopyTo(previewer.ColorSet.AsSpan().Slice(LiveColorSetPreviewer.TextureWidth * 4 * rowIdx, LiveColorSetPreviewer.TextureWidth * 4));
Buffer.MemoryCopy(&row, pDest + LiveColorSetPreviewer.TextureWidth * 4 * rowIdx, LiveColorSetPreviewer.TextureWidth * 4 * sizeof(Half), sizeof(MtrlFile.ColorSet.Row));
previewer.ScheduleUpdate(); previewer.ScheduleUpdate();
} }
} }
public unsafe void UpdateColorSetPreview() public void UpdateColorSetPreview()
{ {
if (ColorSetPreviewers.Count == 0) if (ColorSetPreviewers.Count == 0)
return; return;
@ -447,7 +610,7 @@ public partial class ModEditWindow
var maybeColorDyeSet = Mtrl.ColorDyeSets.FirstOrNull(colorDyeSet => colorDyeSet.Index == colorSet.Index); var maybeColorDyeSet = Mtrl.ColorDyeSets.FirstOrNull(colorDyeSet => colorDyeSet.Index == colorSet.Index);
var rows = colorSet.Rows; var rows = colorSet.Rows;
if (maybeColorDyeSet.HasValue) if (maybeColorDyeSet.HasValue && UseColorDyeSet)
{ {
var stm = _edit._stainService.StmFile; var stm = _edit._stainService.StmFile;
var stainId = (StainId)_edit._stainService.StainCombo.CurrentSelection.Key; var stainId = (StainId)_edit._stainService.StainCombo.CurrentSelection.Key;
@ -457,7 +620,7 @@ public partial class ModEditWindow
ref var row = ref rows[i]; ref var row = ref rows[i];
var dye = colorDyeSet.Rows[i]; var dye = colorDyeSet.Rows[i];
if (stm.TryGetValue(dye.Template, stainId, out var dyes)) if (stm.TryGetValue(dye.Template, stainId, out var dyes))
ApplyDye(ref row, dye, dyes); row.ApplyDyeTemplate(dye, dyes);
} }
} }
@ -466,26 +629,11 @@ public partial class ModEditWindow
foreach (var previewer in ColorSetPreviewers) foreach (var previewer in ColorSetPreviewers)
{ {
fixed (Half* pDest = previewer.ColorSet) rows.AsHalves().CopyTo(previewer.ColorSet);
Buffer.MemoryCopy(&rows, pDest, LiveColorSetPreviewer.TextureLength * sizeof(Half), sizeof(MtrlFile.ColorSet.RowArray));
previewer.ScheduleUpdate(); 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) private static void ApplyHighlight(ref MtrlFile.ColorSet.Row row, int time)
{ {
var level = Math.Sin(time * Math.PI / 16) * 0.5 + 0.5; var level = Math.Sin(time * Math.PI / 16) * 0.5 + 0.5;
@ -498,18 +646,19 @@ public partial class ModEditWindow
public void Update() public void Update()
{ {
UpdateTextureLabels(); UpdateShaders();
UpdateShaderKeyLabels(); UpdateTextures();
UpdateConstantLabels(); UpdateConstants();
UpdateSamplers();
} }
public MtrlTab( ModEditWindow edit, MtrlFile file, string filePath, bool writable ) public MtrlTab( ModEditWindow edit, MtrlFile file, string filePath, bool writable )
{ {
_edit = edit; _edit = edit;
Mtrl = file; Mtrl = file;
FilePath = filePath; FilePath = filePath;
Writable = writable; Writable = writable;
UseColorDyeSet = file.ColorDyeSets.Length > 0;
AssociatedBaseDevkit = TryLoadShpkDevkit( "_base", out LoadedBaseDevkitPathName );
LoadShpk( FindAssociatedShpk( out _, out _ ) ); LoadShpk( FindAssociatedShpk( out _, out _ ) );
if (writable) if (writable)
BindToMaterialInstances(); BindToMaterialInstances();
@ -532,9 +681,96 @@ public partial class ModEditWindow
} }
public bool Valid public bool Valid
=> Mtrl.Valid; => ShadersKnown && Mtrl.Valid;
public byte[] Write() public byte[] Write()
=> Mtrl.Write(); {
var output = Mtrl.Clone();
output.GarbageCollect(AssociatedShpk, SamplerIds, UseColorDyeSet);
return output.Write();
}
private sealed class DevkitShaderKeyValue
{
public string Label = string.Empty;
public string Description = string.Empty;
}
private sealed class DevkitShaderKey
{
public string Label = string.Empty;
public string Description = string.Empty;
public Dictionary<uint, DevkitShaderKeyValue> Values = new();
}
private sealed class DevkitSampler
{
public string Label = string.Empty;
public string Description = string.Empty;
public string DefaultTexture = string.Empty;
}
private enum DevkitConstantType
{
Hidden = -1,
Float = 0,
Integer = 1,
Color = 2,
Enum = 3,
}
private sealed class DevkitConstantValue
{
public string Label = string.Empty;
public string Description = string.Empty;
public float Value = 0.0f;
}
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()
{
switch (Type)
{
case DevkitConstantType.Hidden:
return null;
case DevkitConstantType.Float:
return new FloatConstantEditor(Minimum, Maximum, Speed ?? 0.1f, RelativeSpeed, Factor, Bias, Precision, Unit);
case DevkitConstantType.Integer:
return new IntConstantEditor(ToInteger(Minimum), ToInteger(Maximum), Speed ?? 0.25f, RelativeSpeed, Factor, Bias, Unit);
case DevkitConstantType.Color:
return new ColorConstantEditor(SquaredRgb, Clamped);
case DevkitConstantType.Enum:
return new EnumConstantEditor(Array.ConvertAll(Values, value => (value.Label, value.Value, value.Description)));
default:
return FloatConstantEditor.Default;
}
}
private int? ToInteger(float? value)
=> value.HasValue ? (int)Math.Clamp(MathF.Round(value.Value), int.MinValue, int.MaxValue) : null;
}
} }
} }

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
@ -7,6 +8,7 @@ using Dalamud.Interface;
using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.ImGuiFileDialog;
using ImGuiNET; using ImGuiNET;
using Lumina.Data.Parsing; using Lumina.Data.Parsing;
using Lumina.Excel.GeneratedSheets;
using OtterGui; using OtterGui;
using OtterGui.Raii; using OtterGui.Raii;
using Penumbra.GameData; using Penumbra.GameData;
@ -19,20 +21,92 @@ public partial class ModEditWindow
{ {
private readonly FileDialogService _fileDialog; 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 string[]
{ {
var ret = false; "3dui.shpk",
ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); // "apricot_decal_dummy.shpk",
if (ImGui.InputText("Shader Package Name", ref tab.Mtrl.ShaderPackage.Name, 63, // "apricot_decal_ring.shpk",
disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)) // "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 string[]
{
"Tile the texture at every UV integer junction.\n\nFor example, for U values between 0 and 3, the texture is repeated three times.",
"Flip the texture at every UV integer junction.\n\nFor U values between 0 and 1, for example, the texture is addressed normally; between 1 and 2, the texture is mirrored; between 2 and 3, the texture is normal again; and so on.",
"Texture coordinates outside the range [0.0, 1.0] are set to the texture color at 0.0 or 1.0, respectively.",
"Texture coordinates outside the range [0.0, 1.0] are set to the border color (generally black).",
};
private static bool DrawPackageNameInput(MtrlTab tab, bool disabled)
{
if (disabled)
{ {
ret = true; ImGui.TextUnformatted("Shader Package: " + tab.Mtrl.ShaderPackage.Name);
tab.AssociatedShpk = null; return false;
tab.LoadedShpkPath = FullPath.Empty;
} }
if (ImGui.IsItemDeactivatedAfterEdit()) var ret = false;
tab.LoadShpk(tab.FindAssociatedShpk(out _, out _)); ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f);
using var c = ImRaii.Combo("Shader Package", tab.Mtrl.ShaderPackage.Name);
if (c)
foreach (var value in tab.GetShpkNames())
{
if (ImGui.Selectable(value, value == tab.Mtrl.ShaderPackage.Name))
{
tab.Mtrl.ShaderPackage.Name = value;
ret = true;
tab.AssociatedShpk = null;
tab.LoadedShpkPath = FullPath.Empty;
tab.LoadShpk(tab.FindAssociatedShpk(out _, out _));
}
}
return ret; return ret;
} }
@ -41,8 +115,8 @@ public partial class ModEditWindow
{ {
var ret = false; var ret = false;
var shpkFlags = (int)tab.Mtrl.ShaderPackage.Flags; var shpkFlags = (int)tab.Mtrl.ShaderPackage.Flags;
ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f);
if (ImGui.InputInt("Shader Package Flags", ref shpkFlags, 0, 0, if (ImGui.InputInt("Shader Flags", ref shpkFlags, 0, 0,
ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))) ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)))
{ {
tab.Mtrl.ShaderPackage.Flags = (uint)shpkFlags; tab.Mtrl.ShaderPackage.Flags = (uint)shpkFlags;
@ -62,6 +136,12 @@ public partial class ModEditWindow
var text = tab.AssociatedShpk == null var text = tab.AssociatedShpk == null
? "Associated .shpk file: None" ? "Associated .shpk file: None"
: $"Associated .shpk file: {tab.LoadedShpkPathName}"; : $"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)); ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
@ -70,6 +150,16 @@ public partial class ModEditWindow
ImGuiUtil.HoverTooltip("Click to copy file path to clipboard."); ImGuiUtil.HoverTooltip("Click to copy file path to clipboard.");
if (ImGui.Selectable(devkitText))
ImGui.SetClipboardText(tab.LoadedShpkDevkitPathName);
ImGuiUtil.HoverTooltip("Click to copy file path to clipboard.");
if (ImGui.Selectable(baseDevkitText))
ImGui.SetClipboardText(tab.LoadedBaseDevkitPathName);
ImGuiUtil.HoverTooltip("Click to copy file path to clipboard.");
if (ImGui.Button("Associate Custom .shpk File")) if (ImGui.Button("Associate Custom .shpk File"))
_fileDialog.OpenFilePicker("Associate Custom .shpk File...", ".shpk", (success, name) => _fileDialog.OpenFilePicker("Associate Custom .shpk File...", ".shpk", (success, name) =>
{ {
@ -94,94 +184,50 @@ public partial class ModEditWindow
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
} }
private static bool DrawShaderKey(MtrlTab tab, bool disabled, ref int idx)
{
var ret = false;
using var t2 = ImRaii.TreeNode(tab.ShaderKeyLabels[idx], disabled ? ImGuiTreeNodeFlags.Leaf : 0);
if (!t2 || disabled)
return ret;
var key = tab.Mtrl.ShaderPackage.ShaderKeys[idx];
var shpkKey = tab.AssociatedShpk?.GetMaterialKeyById(key.Category);
if (shpkKey.HasValue)
{
ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f);
using var c = ImRaii.Combo("Value", $"0x{key.Value:X8}");
if (c)
foreach (var value in shpkKey.Value.Values)
{
if (ImGui.Selectable($"0x{value:X8}", value == key.Value))
{
tab.Mtrl.ShaderPackage.ShaderKeys[idx].Value = value;
ret = true;
tab.UpdateShaderKeyLabels();
}
}
}
if (ImGui.Button("Remove Key"))
{
tab.Mtrl.ShaderPackage.ShaderKeys = tab.Mtrl.ShaderPackage.ShaderKeys.RemoveItems(idx--);
ret = true;
tab.UpdateShaderKeyLabels();
}
return ret;
}
private static bool DrawNewShaderKey(MtrlTab tab)
{
ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f);
var ret = false;
using (var c = ImRaii.Combo("##NewConstantId", $"ID: 0x{tab.NewKeyId:X8}"))
{
if (c)
foreach (var idx in tab.MissingShaderKeyIndices)
{
var key = tab.AssociatedShpk!.MaterialKeys[idx];
if (ImGui.Selectable($"ID: 0x{key.Id:X8}", key.Id == tab.NewKeyId))
{
tab.NewKeyDefault = key.DefaultValue;
tab.NewKeyId = key.Id;
ret = true;
tab.UpdateShaderKeyLabels();
}
}
}
ImGui.SameLine();
if (ImGui.Button("Add Key"))
{
tab.Mtrl.ShaderPackage.ShaderKeys = tab.Mtrl.ShaderPackage.ShaderKeys.AddItem(new ShaderKey
{
Category = tab.NewKeyId,
Value = tab.NewKeyDefault,
});
ret = true;
tab.UpdateShaderKeyLabels();
}
return ret;
}
private static bool DrawMaterialShaderKeys(MtrlTab tab, bool disabled) private static bool DrawMaterialShaderKeys(MtrlTab tab, bool disabled)
{ {
if (tab.Mtrl.ShaderPackage.ShaderKeys.Length <= 0 if (tab.ShaderKeys.Count == 0)
&& (disabled || tab.AssociatedShpk == null || tab.AssociatedShpk.MaterialKeys.Length <= 0))
return false;
using var t = ImRaii.TreeNode("Shader Keys");
if (!t)
return false; return false;
var ret = false; var ret = false;
for (var idx = 0; idx < tab.Mtrl.ShaderPackage.ShaderKeys.Length; ++idx) foreach (var (label, index, description, monoFont, values) in tab.ShaderKeys)
ret |= DrawShaderKey(tab, disabled, ref idx); {
using var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont);
ref var key = ref tab.Mtrl.ShaderPackage.ShaderKeys[index];
var shpkKey = tab.AssociatedShpk?.GetMaterialKeyById(key.Category);
var currentValue = key.Value;
var (currentLabel, _, currentDescription) = values.FirstOrNull(v => v.Value == currentValue) ?? ($"0x{currentValue:X8}", currentValue, string.Empty);
if (!disabled && shpkKey.HasValue)
{
ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f);
using (var c = ImRaii.Combo($"##{key.Category:X8}", currentLabel))
{
if (c)
foreach (var (valueLabel, value, valueDescription) in values)
{
if (ImGui.Selectable(valueLabel, value == currentValue))
{
key.Value = value;
ret = true;
tab.Update();
}
if (!disabled && tab.AssociatedShpk != null && tab.MissingShaderKeyIndices.Count != 0) if (valueDescription.Length > 0)
ret |= DrawNewShaderKey(tab); ImGuiUtil.SelectableHelpMarker(valueDescription);
}
}
ImGui.SameLine();
if (description.Length > 0)
ImGuiUtil.LabeledHelpMarker(label, description);
else
ImGui.TextUnformatted(label);
}
else if (description.Length > 0 || currentDescription.Length > 0)
ImGuiUtil.LabeledHelpMarker($"{label}: {currentLabel}",
description + ((description.Length > 0 && currentDescription.Length > 0) ? "\n\n" : string.Empty) + currentDescription);
else
ImGui.TextUnformatted($"{label}: {currentLabel}");
}
return ret; return ret;
} }
@ -191,162 +237,64 @@ public partial class ModEditWindow
if (tab.AssociatedShpk == null) if (tab.AssociatedShpk == null)
return; return;
ImRaii.TreeNode(tab.VertexShaders, ImGuiTreeNodeFlags.Leaf).Dispose(); ImRaii.TreeNode(tab.VertexShadersString, ImGuiTreeNodeFlags.Leaf).Dispose();
ImRaii.TreeNode(tab.PixelShaders, ImGuiTreeNodeFlags.Leaf).Dispose(); ImRaii.TreeNode(tab.PixelShadersString, ImGuiTreeNodeFlags.Leaf).Dispose();
}
if (tab.ShaderComment.Length > 0)
private static bool DrawMaterialConstantValues(MtrlTab tab, bool disabled, ref int idx)
{
var (name, componentOnly, paramValueOffset) = tab.MaterialConstants[idx];
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
using var t2 = ImRaii.TreeNode(name);
if (!t2)
return false;
font.Dispose();
var constant = tab.Mtrl.ShaderPackage.Constants[idx];
var ret = false;
var values = tab.Mtrl.GetConstantValues(constant);
if (values.Length > 0)
{ {
var valueOffset = constant.ByteOffset >> 2; ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
ImGui.TextUnformatted(tab.ShaderComment);
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();
tab.SetMaterialParameter(constant.Id, valueIdx, values.Slice(valueIdx, 1));
}
}
} }
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();
tab.SetMaterialParameter(constant.Id, 0, new float[constant.ByteSize >> 2]);
}
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) private static bool DrawMaterialConstants(MtrlTab tab, bool disabled)
{ {
if (tab.Mtrl.ShaderPackage.Constants.Length == 0 if (tab.Constants.Count == 0)
&& tab.Mtrl.ShaderPackage.ShaderValues.Length == 0
&& (disabled || tab.AssociatedShpk == null || tab.AssociatedShpk.MaterialParams.Length == 0))
return false; return false;
using var font = ImRaii.PushFont(UiBuilder.MonoFont); ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
using var t = ImRaii.TreeNode(tab.MaterialConstantLabel); if (!ImGui.CollapsingHeader("Material Constants"))
if (!t)
return false; return false;
font.Dispose(); using var _ = ImRaii.PushId("MaterialConstants");
var ret = false; var ret = false;
for (var idx = 0; idx < tab.Mtrl.ShaderPackage.Constants.Length; ++idx) foreach (var (header, group) in tab.Constants)
ret |= DrawMaterialConstantValues(tab, disabled, ref idx); {
using var t = ImRaii.TreeNode(header, ImGuiTreeNodeFlags.DefaultOpen);
if (!t)
continue;
if (tab.OrphanedMaterialValues.Count > 0) foreach (var (label, constantIndex, slice, description, monoFont, editor) in group)
ret |= DrawMaterialOrphans(tab, disabled); {
else if (!disabled && !tab.HasMalformedMaterialConstants && tab.MissingMaterialConstants.Count > 0) var constant = tab.Mtrl.ShaderPackage.Constants[constantIndex];
ret |= DrawNewMaterialParam(tab); 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; 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, samplerCrc) = tab.Samplers[idx]; var ret = false;
using var tree = ImRaii.TreeNode(label); ref var texture = ref tab.Mtrl.Textures[textureIdx];
if (!tree) ref var sampler = ref tab.Mtrl.ShaderPackage.Samplers[samplerIdx];
return false;
ImRaii.TreeNode(filename, ImGuiTreeNodeFlags.Leaf).Dispose();
var ret = false;
var sampler = tab.Mtrl.ShaderPackage.Samplers[idx];
// FIXME this probably doesn't belong here // FIXME this probably doesn't belong here
static unsafe bool InputHexUInt16(string label, ref ushort v, ImGuiInputTextFlags flags) static unsafe bool InputHexUInt16(string label, ref ushort v, ImGuiInputTextFlags flags)
@ -357,128 +305,123 @@ public partial class ModEditWindow
} }
} }
ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); static bool ComboTextureAddressMode(string label, ref uint samplerFlags, int bitOffset)
if (InputHexUInt16("Texture Flags", ref tab.Mtrl.Textures[sampler.TextureIndex].Flags, {
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)) disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))
ret = true; ret = true;
var samplerFlags = (int)sampler.Flags; 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, if (ImGui.InputInt("Sampler Flags", ref samplerFlags, 0, 0,
ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))) ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)))
{ {
tab.Mtrl.ShaderPackage.Samplers[idx].Flags = (uint)samplerFlags; sampler.Flags = (uint)samplerFlags;
ret = true; ret = true;
tab.SetSamplerFlags(samplerCrc, (uint)samplerFlags); tab.SetSamplerFlags(sampler.SamplerId, (uint)samplerFlags);
}
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();
} }
return ret; return ret;
} }
private static bool DrawMaterialNewSampler(MtrlTab tab) private bool DrawMaterialShader(MtrlTab tab, bool disabled)
{
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;
var newSamplerId = tab.NewSamplerId;
tab.Mtrl.ShaderPackage.Samplers = tab.Mtrl.ShaderPackage.Samplers.AddItem(new Sampler
{
SamplerId = 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();
tab.SetSamplerFlags(newSamplerId, 0);
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)
{ {
var ret = false; var ret = false;
if (!ImGui.CollapsingHeader("Advanced Shader Resources")) if (ImGui.CollapsingHeader(tab.ShaderHeader))
return ret; {
ret |= DrawPackageNameInput(tab, disabled);
ret |= DrawShaderFlagsInput(tab, disabled);
DrawCustomAssociations(tab);
ret |= DrawMaterialShaderKeys(tab, disabled);
DrawMaterialShaders(tab);
}
if (tab.AssociatedShpkDevkit == null)
{
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
GC.KeepAlive(tab);
var textColor = ImGui.GetColorU32(ImGuiCol.Text);
var textColorWarning = (textColor & 0xFF000000u) | ((textColor & 0x00FEFEFE) >> 1) | (tab.AssociatedShpk == null ? 0x80u : 0x8080u); // Half red or yellow
using var c = ImRaii.PushColor(ImGuiCol.Text, textColorWarning);
ImGui.TextUnformatted(tab.AssociatedShpk == null
? "Unable to find a suitable .shpk file for cross-references. Some functionality will be missing."
: "No dev-kit file found for this material's shaders. Please install one for optimal editing experience, such as actual constant names instead of hexadecimal identifiers.");
}
ret |= DrawPackageNameInput(tab, disabled);
ret |= DrawShaderFlagsInput(tab, disabled);
DrawCustomAssociations(tab);
ret |= DrawMaterialShaderKeys(tab, disabled);
DrawMaterialShaders(tab);
ret |= DrawMaterialConstants(tab, disabled);
ret |= DrawMaterialSamplers(tab, disabled);
return ret; return ret;
} }
@ -500,26 +443,25 @@ public partial class ModEditWindow
_ => null, _ => null,
}; };
} }
private static string VectorSwizzle(int firstComponent, int lastComponent)
=> (firstComponent, lastComponent) switch
{
(0, 4) => " ",
(0, 0) => ".x ",
(0, 1) => ".xy ",
(0, 2) => ".xyz ",
(0, 3) => " ",
(1, 1) => ".y ",
(1, 2) => ".yz ",
(1, 3) => ".yzw ",
(2, 2) => ".z ",
(2, 3) => ".zw ",
(3, 3) => ".w ",
_ => string.Empty,
};
private static (string? Name, bool ComponentOnly) MaterialParamRangeName(string prefix, int valueOffset, int valueLength) private static (string? Name, bool ComponentOnly) MaterialParamRangeName(string prefix, int valueOffset, int valueLength)
{ {
static string VectorSwizzle(int firstComponent, int lastComponent)
=> (firstComponent, lastComponent) switch
{
(0, 4) => " ",
(0, 0) => ".x ",
(0, 1) => ".xy ",
(0, 2) => ".xyz ",
(0, 3) => " ",
(1, 1) => ".y ",
(1, 2) => ".yz ",
(1, 3) => ".yzw ",
(2, 2) => ".z ",
(2, 3) => ".zw ",
(3, 3) => ".w ",
_ => string.Empty,
};
if (valueLength == 0 || valueOffset < 0) if (valueLength == 0 || valueOffset < 0)
return (null, false); return (null, false);

View file

@ -18,16 +18,14 @@ public partial class ModEditWindow
DrawMaterialLivePreviewRebind( tab, disabled ); DrawMaterialLivePreviewRebind( tab, disabled );
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
var ret = DrawMaterialTextureChange( tab, disabled ); var ret = DrawBackFaceAndTransparency( tab, disabled );
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
ret |= DrawBackFaceAndTransparency( tab, disabled ); ret |= DrawMaterialShader( tab, disabled );
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); ret |= DrawMaterialTextureChange( tab, disabled );
ret |= DrawMaterialColorSetChange( tab, disabled ); ret |= DrawMaterialColorSetChange( tab, disabled );
ret |= DrawMaterialConstants( tab, disabled );
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
ret |= DrawMaterialShaderResources( tab, disabled );
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
DrawOtherMaterialDetails( tab.Mtrl, disabled ); DrawOtherMaterialDetails( tab.Mtrl, disabled );
@ -40,35 +38,87 @@ public partial class ModEditWindow
if (disabled) if (disabled)
return; return;
if (ImGui.Button("Reload live-preview")) if (ImGui.Button("Reload live preview"))
tab.BindToMaterialInstances(); tab.BindToMaterialInstances();
if (tab.MaterialPreviewers.Count == 0 && tab.ColorSetPreviewers.Count == 0)
{
ImGui.SameLine();
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("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 ) private static bool DrawMaterialTextureChange( MtrlTab tab, bool disabled )
{ {
var ret = false; if( tab.Textures.Count == 0 )
using var table = ImRaii.Table( "##Textures", 2 );
ImGui.TableSetupColumn( "Path", ImGuiTableColumnFlags.WidthStretch );
ImGui.TableSetupColumn( "Name", ImGuiTableColumnFlags.WidthFixed, tab.TextureLabelWidth * UiHelpers.Scale );
for( var i = 0; i < tab.Mtrl.Textures.Length; ++i )
{ {
using var _ = ImRaii.PushId( i ); return false;
var tmp = tab.Mtrl.Textures[ i ].Path; }
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
if( !ImGui.CollapsingHeader( "Textures and Samplers", ImGuiTreeNodeFlags.DefaultOpen ) )
{
return false;
}
var frameHeight = ImGui.GetFrameHeight();
var ret = false;
using var table = ImRaii.Table( "##Textures", 3 );
ImGui.TableSetupColumn( string.Empty, ImGuiTableColumnFlags.WidthFixed, frameHeight );
ImGui.TableSetupColumn( "Path" , ImGuiTableColumnFlags.WidthStretch );
ImGui.TableSetupColumn( "Name" , ImGuiTableColumnFlags.WidthFixed, tab.TextureLabelWidth * UiHelpers.Scale );
for( var i = 0; i < tab.Textures.Count; ++i )
{
var (label, textureI, samplerI, description, monoFont) = tab.Textures[i];
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.TableNextColumn();
ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X ); ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X );
if( ImGui.InputText( string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength, if( ImGui.InputText( string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength,
disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None )
&& tmp.Length > 0 && tmp.Length > 0
&& tmp != tab.Mtrl.Textures[ i ].Path ) && tmp != tab.Mtrl.Textures[ textureI ].Path )
{ {
ret = true; ret = true;
tab.Mtrl.Textures[ i ].Path = tmp; tab.Mtrl.Textures[ textureI ].Path = tmp;
} }
ImGui.TableNextColumn(); ImGui.TableNextColumn();
using var font = ImRaii.PushFont( UiBuilder.MonoFont ); using( var font = ImRaii.PushFont( UiBuilder.MonoFont, monoFont ) )
ImGui.AlignTextToFramePadding(); {
ImGui.TextUnformatted( tab.TextureLabels[ i ] ); ImGui.AlignTextToFramePadding();
if( description.Length > 0 )
ImGuiUtil.LabeledHelpMarker( label, description );
else
ImGui.TextUnformatted( label );
}
if( unfolded )
{
ImGui.TableNextColumn();
ImGui.TableNextColumn();
ret |= DrawMaterialSampler( tab, disabled, textureI, samplerI );
ImGui.TableNextColumn();
}
} }
return ret; return ret;

View file

@ -14,7 +14,6 @@ using Penumbra.GameData;
using Penumbra.GameData.Data; using Penumbra.GameData.Data;
using Penumbra.GameData.Files; using Penumbra.GameData.Files;
using Penumbra.String; using Penumbra.String;
using Penumbra.UI.AdvancedWindow;
using static Penumbra.GameData.Files.ShpkFile; using static Penumbra.GameData.Files.ShpkFile;
namespace Penumbra.UI.AdvancedWindow; namespace Penumbra.UI.AdvancedWindow;
@ -40,7 +39,13 @@ public partial class ModEditWindow
ret |= DrawShaderPackageMaterialParamLayout( file, disabled ); ret |= DrawShaderPackageMaterialParamLayout( file, disabled );
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); 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(); file.FileDialog.Draw();
@ -50,7 +55,18 @@ public partial class ModEditWindow
} }
private static void DrawShaderPackageSummary( ShpkTab tab ) 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 ) private static void DrawShaderExportButton( ShpkTab tab, string objectName, Shader shader, int idx )
{ {
@ -163,7 +179,7 @@ public partial class ModEditWindow
} }
DrawShaderExportButton( tab, objectName, shader, idx ); DrawShaderExportButton( tab, objectName, shader, idx );
if( !disabled ) if( !disabled && tab.Shpk.Disassembled )
{ {
ImGui.SameLine(); ImGui.SameLine();
DrawShaderImportButton( tab, objectName, shaders, idx ); DrawShaderImportButton( tab, objectName, shaders, idx );
@ -182,7 +198,8 @@ public partial class ModEditWindow
} }
} }
DrawRawDisassembly( shader ); if( tab.Shpk.Disassembled )
DrawRawDisassembly( shader );
} }
return ret; return ret;
@ -276,7 +293,9 @@ public partial class ModEditWindow
private static bool DrawShaderPackageMaterialMatrix( ShpkTab tab, bool disabled ) private static bool DrawShaderPackageMaterialMatrix( ShpkTab tab, bool disabled )
{ {
ImGui.TextUnformatted( "Parameter positions (continuations are grayed out, unused values are red):" ); ImGui.TextUnformatted( tab.Shpk.Disassembled
? "Parameter positions (continuations are grayed out, unused values are red):"
: "Parameter positions (continuations are grayed out):" );
using var table = ImRaii.Table( "##MaterialParamLayout", 5, using var table = ImRaii.Table( "##MaterialParamLayout", 5,
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ); ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg );
@ -471,6 +490,22 @@ public partial class ModEditWindow
return ret; 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 ) private static void DrawKeyArray( string arrayName, bool withId, IReadOnlyCollection< Key > keys )
{ {
if( keys.Count == 0 ) if( keys.Count == 0 )
@ -513,7 +548,7 @@ public partial class ModEditWindow
foreach( var (node, idx) in tab.Shpk.Nodes.WithIndex() ) foreach( var (node, idx) in tab.Shpk.Nodes.WithIndex() )
{ {
using var font = ImRaii.PushFont( UiBuilder.MonoFont ); using var 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 ) if( !t2 )
{ {
continue; continue;
@ -549,39 +584,38 @@ public partial class ModEditWindow
} }
} }
private static bool DrawOtherShaderPackageDetails( ShpkTab tab, bool disabled ) private static void DrawShaderPackageSelection( ShpkTab tab )
{ {
var ret = false; if( !ImGui.CollapsingHeader( "Shader Selection" ) )
if( !ImGui.CollapsingHeader( "Further Content" ) )
{ {
return false; return;
} }
ImRaii.TreeNode( $"Version: 0x{tab.Shpk.Version:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose();
ret |= DrawShaderPackageResourceArray( "Constant Buffers", "type", true, tab.Shpk.Constants, disabled );
ret |= DrawShaderPackageResourceArray( "Samplers", "type", false, tab.Shpk.Samplers, disabled );
ret |= DrawShaderPackageResourceArray( "Unordered Access Views", "type", false, tab.Shpk.Uavs, disabled );
DrawKeyArray( "System Keys", true, tab.Shpk.SystemKeys ); DrawKeyArray( "System Keys", true, tab.Shpk.SystemKeys );
DrawKeyArray( "Scene Keys", true, tab.Shpk.SceneKeys ); DrawKeyArray( "Scene Keys", true, tab.Shpk.SceneKeys );
DrawKeyArray( "Material Keys", true, tab.Shpk.MaterialKeys ); DrawKeyArray( "Material Keys", true, tab.Shpk.MaterialKeys );
DrawKeyArray( "Sub-View Keys", false, tab.Shpk.SubViewKeys ); DrawKeyArray( "Sub-View Keys", false, tab.Shpk.SubViewKeys );
DrawShaderPackageNodes( tab ); DrawShaderPackageNodes( tab );
if( tab.Shpk.Items.Length > 0 ) using var t = ImRaii.TreeNode( $"Node Selectors ({tab.Shpk.NodeSelectors.Count})###NodeSelectors" );
if( t )
{ {
using var t = ImRaii.TreeNode( $"Items ({tab.Shpk.Items.Length})###Items" ); using var font = ImRaii.PushFont( UiBuilder.MonoFont );
if( t ) foreach( var selector in tab.Shpk.NodeSelectors )
{ {
using var font = ImRaii.PushFont( UiBuilder.MonoFont ); ImRaii.TreeNode( $"#{selector.Value:D4}: Selector: 0x{selector.Key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose();
foreach( var (item, idx) in tab.Shpk.Items.WithIndex() )
{
ImRaii.TreeNode( $"#{idx:D4}: ID: 0x{item.Id:X8}, node: {item.Node}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose();
}
} }
} }
}
private static void DrawOtherShaderPackageDetails( ShpkTab tab )
{
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 ) if( tab.Shpk.AdditionalData.Length > 0 )
{ {
@ -591,8 +625,6 @@ public partial class ModEditWindow
ImGuiUtil.TextWrapped( string.Join( ' ', tab.Shpk.AdditionalData.Select( c => $"{c:X2}" ) ) ); ImGuiUtil.TextWrapped( string.Join( ' ', tab.Shpk.AdditionalData.Select( c => $"{c:X2}" ) ) );
} }
} }
return ret;
} }
private static string UsedComponentString( bool withSize, in Resource resource ) private static string UsedComponentString( bool withSize, in Resource resource )

View file

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

View file

@ -1,4 +1,6 @@
using System; using System;
using System.Collections;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
@ -21,6 +23,7 @@ using Penumbra.Meta;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.String;
using Penumbra.String.Classes; using Penumbra.String.Classes;
using Penumbra.UI.Classes; using Penumbra.UI.Classes;
using Penumbra.Util; using Penumbra.Util;
@ -523,6 +526,23 @@ public partial class ModEditWindow : Window, IDisposable
return new FullPath(path); 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, public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, IDataManager gameData,
Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager, Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager,
StainService stainService, ActiveCollections activeCollections, DalamudServices dalamud, ModMergeTab modMergeTab, StainService stainService, ActiveCollections activeCollections, DalamudServices dalamud, ModMergeTab modMergeTab,

View file

@ -195,21 +195,7 @@ public class ModPanelSettingsTab : ITab
_collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, (uint)idx2); _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, (uint)idx2);
if (option.Description.Length > 0) if (option.Description.Length > 0)
{ ImGuiUtil.SelectableHelpMarker(option.Description);
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);
}
}
id.Pop(); id.Pop();
} }