mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Merge branch 'mdl-attributes'
This commit is contained in:
commit
19866c5638
3 changed files with 315 additions and 16 deletions
103
Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs
Normal file
103
Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
using OtterGui;
|
||||
using Penumbra.GameData;
|
||||
using Penumbra.GameData.Files;
|
||||
|
||||
namespace Penumbra.UI.AdvancedWindow;
|
||||
|
||||
public partial class ModEditWindow
|
||||
{
|
||||
private class MdlTab : IWritable
|
||||
{
|
||||
public readonly MdlFile Mdl;
|
||||
|
||||
private readonly List<string>[] _attributes;
|
||||
|
||||
public MdlTab(byte[] bytes)
|
||||
{
|
||||
Mdl = new MdlFile(bytes);
|
||||
_attributes = CreateAttributes(Mdl);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Valid
|
||||
=> Mdl.Valid;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public byte[] Write()
|
||||
=> Mdl.Write();
|
||||
|
||||
/// <summary> Remove the material given by the index. </summary>
|
||||
/// <remarks> Meshes using the removed material are redirected to material 0, and those after the index are corrected. </remarks>
|
||||
public void RemoveMaterial(int materialIndex)
|
||||
{
|
||||
for (var meshIndex = 0; meshIndex < Mdl.Meshes.Length; meshIndex++)
|
||||
{
|
||||
var newIndex = Mdl.Meshes[meshIndex].MaterialIndex;
|
||||
if (newIndex == materialIndex)
|
||||
newIndex = 0;
|
||||
else if (newIndex > materialIndex)
|
||||
--newIndex;
|
||||
|
||||
Mdl.Meshes[meshIndex].MaterialIndex = newIndex;
|
||||
}
|
||||
|
||||
Mdl.Materials = Mdl.Materials.RemoveItems(materialIndex);
|
||||
}
|
||||
|
||||
/// <summary> Create a list of attributes per sub mesh. </summary>
|
||||
private static List<string>[] CreateAttributes(MdlFile mdl)
|
||||
=> mdl.SubMeshes.Select(s => Enumerable.Range(0, 32)
|
||||
.Where(idx => ((s.AttributeIndexMask >> idx) & 1) == 1)
|
||||
.Select(idx => mdl.Attributes[idx])
|
||||
.ToList()
|
||||
).ToArray();
|
||||
|
||||
/// <summary> Obtain the attributes associated with a sub mesh by its index. </summary>
|
||||
public IReadOnlyList<string> GetSubMeshAttributes(int subMeshIndex)
|
||||
=> _attributes[subMeshIndex];
|
||||
|
||||
/// <summary> Remove or add attributes from a sub mesh by its index. </summary>
|
||||
/// <param name="subMeshIndex"> The index of the sub mesh to update. </param>
|
||||
/// <param name="old"> If non-null, remove this attribute. </param>
|
||||
/// <param name="new"> If non-null, add this attribute. </param>
|
||||
public void UpdateSubMeshAttribute(int subMeshIndex, string? old, string? @new)
|
||||
{
|
||||
var attributes = _attributes[subMeshIndex];
|
||||
|
||||
if (old != null)
|
||||
attributes.Remove(old);
|
||||
|
||||
if (@new != null)
|
||||
attributes.Add(@new);
|
||||
|
||||
PersistAttributes();
|
||||
}
|
||||
|
||||
/// <summary> Apply changes to attributes to the file in memory. </summary>
|
||||
private void PersistAttributes()
|
||||
{
|
||||
var allAttributes = new List<string>();
|
||||
|
||||
foreach (var (attributes, subMeshIndex) in _attributes.WithIndex())
|
||||
{
|
||||
var mask = 0u;
|
||||
|
||||
foreach (var attribute in attributes)
|
||||
{
|
||||
var attributeIndex = allAttributes.IndexOf(attribute);
|
||||
if (attributeIndex == -1)
|
||||
{
|
||||
allAttributes.Add(attribute);
|
||||
attributeIndex = allAttributes.Count - 1;
|
||||
}
|
||||
|
||||
mask |= 1u << attributeIndex;
|
||||
}
|
||||
|
||||
Mdl.SubMeshes[subMeshIndex].AttributeIndexMask = mask;
|
||||
}
|
||||
|
||||
Mdl.Attributes = [.. allAttributes];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
using Dalamud.Interface;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Widgets;
|
||||
using Penumbra.GameData;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.String.Classes;
|
||||
|
||||
|
|
@ -8,30 +11,216 @@ namespace Penumbra.UI.AdvancedWindow;
|
|||
|
||||
public partial class ModEditWindow
|
||||
{
|
||||
private readonly FileEditor<MdlFile> _modelTab;
|
||||
private const int MdlMaterialMaximum = 4;
|
||||
|
||||
private static bool DrawModelPanel(MdlFile file, bool disabled)
|
||||
private readonly FileEditor<MdlTab> _modelTab;
|
||||
|
||||
private string _modelNewMaterial = string.Empty;
|
||||
private readonly List<TagButtons> _subMeshAttributeTagWidgets = [];
|
||||
|
||||
private bool DrawModelPanel(MdlTab tab, bool disabled)
|
||||
{
|
||||
var ret = false;
|
||||
for (var i = 0; i < file.Materials.Length; ++i)
|
||||
var file = tab.Mdl;
|
||||
|
||||
var subMeshTotal = file.Meshes.Aggregate(0, (count, mesh) => count + mesh.SubMeshCount);
|
||||
if (_subMeshAttributeTagWidgets.Count != subMeshTotal)
|
||||
{
|
||||
using var id = ImRaii.PushId(i);
|
||||
var tmp = file.Materials[i];
|
||||
if (ImGui.InputText(string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength,
|
||||
disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)
|
||||
&& tmp.Length > 0
|
||||
&& tmp != file.Materials[i])
|
||||
{
|
||||
file.Materials[i] = tmp;
|
||||
ret = true;
|
||||
}
|
||||
_subMeshAttributeTagWidgets.Clear();
|
||||
_subMeshAttributeTagWidgets.AddRange(
|
||||
Enumerable.Range(0, subMeshTotal).Select(_ => new TagButtons())
|
||||
);
|
||||
}
|
||||
|
||||
var ret = false;
|
||||
|
||||
ret |= DrawModelMaterialDetails(tab, disabled);
|
||||
|
||||
if (ImGui.CollapsingHeader($"Meshes ({file.Meshes.Length})###meshes"))
|
||||
for (var i = 0; i < file.LodCount; ++i)
|
||||
ret |= DrawModelLodDetails(tab, i, disabled);
|
||||
|
||||
ret |= DrawOtherModelDetails(file, disabled);
|
||||
|
||||
return !disabled && ret;
|
||||
}
|
||||
|
||||
private bool DrawModelMaterialDetails(MdlTab tab, bool disabled)
|
||||
{
|
||||
if (!ImGui.CollapsingHeader("Materials"))
|
||||
return false;
|
||||
|
||||
using var table = ImRaii.Table(string.Empty, disabled ? 2 : 3, ImGuiTableFlags.SizingFixedFit);
|
||||
if (!table)
|
||||
return false;
|
||||
|
||||
var ret = false;
|
||||
var materials = tab.Mdl.Materials;
|
||||
|
||||
ImGui.TableSetupColumn("index", ImGuiTableColumnFlags.WidthFixed, 80 * UiHelpers.Scale);
|
||||
ImGui.TableSetupColumn("path", ImGuiTableColumnFlags.WidthStretch, 1);
|
||||
if (!disabled)
|
||||
ImGui.TableSetupColumn("actions", ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X);
|
||||
|
||||
var inputFlags = disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None;
|
||||
for (var materialIndex = 0; materialIndex < materials.Length; materialIndex++)
|
||||
ret |= DrawMaterialRow(tab, disabled, materials, materialIndex, inputFlags);
|
||||
|
||||
if (materials.Length >= MdlMaterialMaximum || disabled)
|
||||
return ret;
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetNextItemWidth(-1);
|
||||
ImGui.InputTextWithHint("##newMaterial", "Add new material...", ref _modelNewMaterial, Utf8GamePath.MaxGamePathLength, inputFlags);
|
||||
var validName = _modelNewMaterial.Length > 0 && _modelNewMaterial[0] == '/';
|
||||
ImGui.TableNextColumn();
|
||||
if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize, string.Empty, !validName, true))
|
||||
return ret;
|
||||
|
||||
tab.Mdl.Materials = materials.AddItem(_modelNewMaterial);
|
||||
_modelNewMaterial = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool DrawMaterialRow(MdlTab tab, bool disabled, string[] materials, int materialIndex, ImGuiInputTextFlags inputFlags)
|
||||
{
|
||||
using var id = ImRaii.PushId(materialIndex);
|
||||
var ret = false;
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted($"Material #{materialIndex + 1}");
|
||||
|
||||
var temp = materials[materialIndex];
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetNextItemWidth(-1);
|
||||
if (ImGui.InputText($"##material{materialIndex}", ref temp, Utf8GamePath.MaxGamePathLength, inputFlags)
|
||||
&& temp.Length > 0
|
||||
&& temp != materials[materialIndex]
|
||||
)
|
||||
{
|
||||
materials[materialIndex] = temp;
|
||||
ret = true;
|
||||
}
|
||||
|
||||
if (disabled)
|
||||
return ret;
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
|
||||
// Need to have at least one material.
|
||||
if (materials.Length <= 1)
|
||||
return ret;
|
||||
|
||||
var tt = "Delete this material.\nAny meshes targeting this material will be updated to use material #1.";
|
||||
var modifierActive = _config.DeleteModModifier.IsActive();
|
||||
if (!modifierActive)
|
||||
tt += $"\nHold {_config.DeleteModModifier} to delete.";
|
||||
if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, tt, !modifierActive, true))
|
||||
return ret;
|
||||
|
||||
tab.RemoveMaterial(materialIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool DrawModelLodDetails(MdlTab tab, int lodIndex, bool disabled)
|
||||
{
|
||||
using var lodNode = ImRaii.TreeNode($"Level of Detail #{lodIndex + 1}", ImGuiTreeNodeFlags.DefaultOpen);
|
||||
if (!lodNode)
|
||||
return false;
|
||||
|
||||
var lod = tab.Mdl.Lods[lodIndex];
|
||||
var ret = false;
|
||||
|
||||
for (var meshOffset = 0; meshOffset < lod.MeshCount; meshOffset++)
|
||||
ret |= DrawModelMeshDetails(tab, lod.MeshIndex + meshOffset, disabled);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private bool DrawModelMeshDetails(MdlTab tab, int meshIndex, bool disabled)
|
||||
{
|
||||
using var meshNode = ImRaii.TreeNode($"Mesh #{meshIndex + 1}", ImGuiTreeNodeFlags.DefaultOpen);
|
||||
if (!meshNode)
|
||||
return false;
|
||||
|
||||
using var id = ImRaii.PushId(meshIndex);
|
||||
using var table = ImRaii.Table(string.Empty, 2, ImGuiTableFlags.SizingFixedFit);
|
||||
if (!table)
|
||||
return false;
|
||||
|
||||
ImGui.TableSetupColumn("name", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale);
|
||||
ImGui.TableSetupColumn("field", ImGuiTableColumnFlags.WidthStretch, 1);
|
||||
|
||||
var file = tab.Mdl;
|
||||
var mesh = file.Meshes[meshIndex];
|
||||
|
||||
|
||||
// Mesh material
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted("Material");
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
var ret = DrawMaterialCombo(tab, meshIndex, disabled);
|
||||
|
||||
// Sub meshes
|
||||
for (var subMeshOffset = 0; subMeshOffset < mesh.SubMeshCount; subMeshOffset++)
|
||||
ret |= DrawSubMeshAttributes(tab, meshIndex, disabled, subMeshOffset);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private bool DrawMaterialCombo(MdlTab tab, int meshIndex, bool disabled)
|
||||
{
|
||||
var mesh = tab.Mdl.Meshes[meshIndex];
|
||||
using var _ = ImRaii.Disabled(disabled);
|
||||
ImGui.SetNextItemWidth(-1);
|
||||
using var materialCombo = ImRaii.Combo("##material", tab.Mdl.Materials[mesh.MaterialIndex]);
|
||||
|
||||
if (!materialCombo)
|
||||
return false;
|
||||
|
||||
var ret = false;
|
||||
foreach (var (material, materialIndex) in tab.Mdl.Materials.WithIndex())
|
||||
{
|
||||
if (!ImGui.Selectable(material, mesh.MaterialIndex == materialIndex))
|
||||
continue;
|
||||
|
||||
tab.Mdl.Meshes[meshIndex].MaterialIndex = (ushort)materialIndex;
|
||||
ret = true;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private bool DrawSubMeshAttributes(MdlTab tab, int meshIndex, bool disabled, int subMeshOffset)
|
||||
{
|
||||
using var _ = ImRaii.PushId(subMeshOffset);
|
||||
|
||||
var mesh = tab.Mdl.Meshes[meshIndex];
|
||||
var subMeshIndex = mesh.SubMeshIndex + subMeshOffset;
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted($"Attributes #{subMeshOffset + 1}");
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
var widget = _subMeshAttributeTagWidgets[subMeshIndex];
|
||||
var attributes = tab.GetSubMeshAttributes(subMeshIndex);
|
||||
|
||||
var tagIndex = widget.Draw(string.Empty, string.Empty, attributes,
|
||||
out var editedAttribute, !disabled);
|
||||
if (tagIndex < 0)
|
||||
return false;
|
||||
|
||||
var oldName = tagIndex < attributes.Count ? attributes[tagIndex] : null;
|
||||
var newName = editedAttribute.Length > 0 ? editedAttribute : null;
|
||||
tab.UpdateSubMeshAttribute(subMeshIndex, oldName, newName);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool DrawOtherModelDetails(MdlFile file, bool _)
|
||||
{
|
||||
if (!ImGui.CollapsingHeader("Further Content"))
|
||||
|
|
@ -82,6 +271,13 @@ public partial class ModEditWindow
|
|||
}
|
||||
}
|
||||
|
||||
using (var materials = ImRaii.TreeNode("Materials", ImGuiTreeNodeFlags.DefaultOpen))
|
||||
{
|
||||
if (materials)
|
||||
foreach (var material in file.Materials)
|
||||
ImRaii.TreeNode(material, ImGuiTreeNodeFlags.Leaf).Dispose();
|
||||
}
|
||||
|
||||
using (var attributes = ImRaii.TreeNode("Attributes", ImGuiTreeNodeFlags.DefaultOpen))
|
||||
{
|
||||
if (attributes)
|
||||
|
|
|
|||
|
|
@ -584,8 +584,8 @@ public partial class ModEditWindow : Window, IDisposable
|
|||
_materialTab = new FileEditor<MtrlTab>(this, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl",
|
||||
() => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty,
|
||||
(bytes, path, writable) => new MtrlTab(this, new MtrlFile(bytes), path, writable));
|
||||
_modelTab = new FileEditor<MdlFile>(this, gameData, config, _editor.Compactor, _fileDialog, "Models", ".mdl",
|
||||
() => PopulateIsOnPlayer(_editor.Files.Mdl, ResourceType.Mdl), DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, _, _) => new MdlFile(bytes));
|
||||
_modelTab = new FileEditor<MdlTab>(this, gameData, config, _editor.Compactor, _fileDialog, "Models", ".mdl",
|
||||
() => PopulateIsOnPlayer(_editor.Files.Mdl, ResourceType.Mdl), DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, _, _) => new MdlTab(bytes));
|
||||
_shaderPackageTab = new FileEditor<ShpkTab>(this, gameData, config, _editor.Compactor, _fileDialog, "Shaders", ".shpk",
|
||||
() => PopulateIsOnPlayer(_editor.Files.Shpk, ResourceType.Shpk), DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty,
|
||||
(bytes, _, _) => new ShpkTab(_fileDialog, bytes));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue