diff --git a/CUE4Parse b/CUE4Parse
index 581d29b2..ae298a23 160000
--- a/CUE4Parse
+++ b/CUE4Parse
@@ -1 +1 @@
-Subproject commit 581d29b275a2581c2b246991fe8e5f988605e0b5
+Subproject commit ae298a23de24f0b05e101a0fc8f0192667f0ed4b
diff --git a/FModel/FModel.csproj b/FModel/FModel.csproj
index 7f804af2..d7eda14a 100644
--- a/FModel/FModel.csproj
+++ b/FModel/FModel.csproj
@@ -227,6 +227,8 @@
+
+
diff --git a/FModel/Resources/link_off.png b/FModel/Resources/link_off.png
new file mode 100644
index 00000000..e0fac9c7
Binary files /dev/null and b/FModel/Resources/link_off.png differ
diff --git a/FModel/Resources/link_on.png b/FModel/Resources/link_on.png
new file mode 100644
index 00000000..d1c0c19d
Binary files /dev/null and b/FModel/Resources/link_on.png differ
diff --git a/FModel/Views/Snooper/Models/Model.cs b/FModel/Views/Snooper/Models/Model.cs
index 723e9b68..da1d763b 100644
--- a/FModel/Views/Snooper/Models/Model.cs
+++ b/FModel/Views/Snooper/Models/Model.cs
@@ -54,6 +54,8 @@ public class Model : IDisposable
public bool HasSockets => Sockets.Length > 0;
public readonly Socket[] Sockets;
+ public bool IsAttached { get; private set; }
+ public string AttachedTo { get; private set; }
public int TransformsCount;
public readonly List Transforms;
@@ -63,7 +65,7 @@ public class Model : IDisposable
public bool Show;
public bool Wireframe;
- public bool IsSetup;
+ public bool IsSetup { get; private set; }
public bool IsSelected;
public int SelectedInstance;
public float MorphTime;
@@ -88,7 +90,7 @@ public class Model : IDisposable
for (int i = 0; i < Sockets.Length; i++)
{
if (export.Sockets[i].Load() is not { } socket) continue;
- Sockets[i] = new Socket(socket);
+ Sockets[i] = new Socket(socket, Transforms[0]);
}
}
private Model(USkeletalMesh export, CSkeletalMesh skeletalMesh, Transform transform) : this(export, export.Materials, export.Skeleton, skeletalMesh.LODs.Count, skeletalMesh.LODs[0], skeletalMesh.LODs[0].Verts, transform)
@@ -106,13 +108,9 @@ public class Model : IDisposable
if (!Skeleton.BonesIndexByName.TryGetValue(socket.BoneName.Text, out var boneIndex) ||
!Skeleton.BonesTransformByIndex.TryGetValue(boneIndex, out var boneTransform))
- {
- Sockets[i] = new Socket(socket);
- }
- else
- {
- Sockets[i] = new Socket(socket, boneTransform);
- }
+ boneTransform = Transforms[0];
+
+ Sockets[i] = new Socket(socket, boneTransform);
}
}
public Model(USkeletalMesh export, CSkeletalMesh skeletalMesh) : this(export, skeletalMesh, Transform.Identity)
@@ -217,6 +215,7 @@ public class Model : IDisposable
if (section.IsValid) Sections[s].SetupMaterial(Materials[section.MaterialIndex]);
}
+ _previousMatrix = t.Matrix;
AddInstance(t);
}
@@ -224,13 +223,12 @@ public class Model : IDisposable
{
TransformsCount++;
Transforms.Add(transform);
- _previousMatrix = transform.Matrix;
}
public void UpdateMatrices(Options options)
{
UpdateMatrices();
- if (!HasSkeleton)
+ if (!HasSockets)
return;
for (int s = 0; s < Sockets.Length; s++)
@@ -271,6 +269,23 @@ public class Model : IDisposable
_morphVbo.Unbind();
}
+ public void AttachModel(Model attachedTo, Socket socket)
+ {
+ IsAttached = true;
+ AttachedTo = $"'{socket.Name}' from '{attachedTo.Name}'{(socket.Bone.HasValue ? $" at '{socket.Bone}'" : "")}";
+ // reset PRS to 0 so it's attached to the actual position (can be transformed relative to the socket later by the user)
+ Transforms[SelectedInstance].Position = FVector.ZeroVector;
+ Transforms[SelectedInstance].Rotation = FQuat.Identity;
+ Transforms[SelectedInstance].Scale = FVector.OneVector;
+ }
+
+ public void DetachModel()
+ {
+ IsAttached = false;
+ AttachedTo = null;
+ Transforms[SelectedInstance].Relation = _previousMatrix;
+ }
+
public void SetupInstances()
{
var instanceMatrix = new Matrix4x4[TransformsCount];
diff --git a/FModel/Views/Snooper/Models/Socket.cs b/FModel/Views/Snooper/Models/Socket.cs
index c1dd0919..a31a7814 100644
--- a/FModel/Views/Snooper/Models/Socket.cs
+++ b/FModel/Views/Snooper/Models/Socket.cs
@@ -4,36 +4,28 @@ using System.Numerics;
using CUE4Parse.UE4.Assets.Exports.SkeletalMesh;
using CUE4Parse.UE4.Assets.Exports.StaticMesh;
using CUE4Parse.UE4.Objects.Core.Misc;
+using CUE4Parse.UE4.Objects.UObject;
namespace FModel.Views.Snooper.Models;
public class Socket : IDisposable
{
public readonly string Name;
- public readonly string Bone;
+ public readonly FName? Bone;
public readonly Transform Transform;
public readonly List AttachedModels;
private Socket()
{
- Bone = "None";
Transform = Transform.Identity;
AttachedModels = new List();
}
- public Socket(UStaticMeshSocket socket) : this()
+ public Socket(UStaticMeshSocket socket, Transform transform) : this()
{
Name = socket.SocketName.Text;
- Transform.Rotation = socket.RelativeRotation.Quaternion();
- Transform.Position = socket.RelativeLocation * Constants.SCALE_DOWN_RATIO;
- Transform.Scale = socket.RelativeScale;
- }
-
- public Socket(USkeletalMeshSocket socket) : this()
- {
- Name = socket.SocketName.Text;
- Bone = socket.BoneName.Text;
+ Transform.Relation = transform.Matrix;
Transform.Rotation = socket.RelativeRotation.Quaternion();
Transform.Position = socket.RelativeLocation * Constants.SCALE_DOWN_RATIO;
Transform.Scale = socket.RelativeScale;
@@ -42,7 +34,7 @@ public class Socket : IDisposable
public Socket(USkeletalMeshSocket socket, Transform transform) : this()
{
Name = socket.SocketName.Text;
- Bone = socket.BoneName.Text;
+ Bone = socket.BoneName;
Transform.Relation = transform.Matrix;
Transform.Rotation = socket.RelativeRotation.Quaternion();
Transform.Position = socket.RelativeLocation * Constants.SCALE_DOWN_RATIO;
diff --git a/FModel/Views/Snooper/Options.cs b/FModel/Views/Snooper/Options.cs
index a3e38bb4..bf233fc1 100644
--- a/FModel/Views/Snooper/Options.cs
+++ b/FModel/Views/Snooper/Options.cs
@@ -36,6 +36,8 @@ public class Options
["noimage"] = new ("T_Placeholder_Item_Image"),
["pointlight"] = new ("pointlight"),
["spotlight"] = new ("spotlight"),
+ ["link_on"] = new ("link_on"),
+ ["link_off"] = new ("link_off"),
};
_platform = UserSettings.Default.OverridedPlatform;
diff --git a/FModel/Views/Snooper/SnimGui.cs b/FModel/Views/Snooper/SnimGui.cs
index 55ec13e0..7749965e 100644
--- a/FModel/Views/Snooper/SnimGui.cs
+++ b/FModel/Views/Snooper/SnimGui.cs
@@ -7,7 +7,6 @@ using ImGuiNET;
using OpenTK.Windowing.Common;
using System.Numerics;
using System.Text;
-using CUE4Parse.UE4.Objects.Core.Math;
using FModel.Settings;
using FModel.Views.Snooper.Models;
using FModel.Views.Snooper.Shading;
@@ -56,6 +55,7 @@ public class SnimGui
private readonly Vector4 _errorColor = new (0.761f, 0.169f, 0.169f, 1.0f);
private const uint _dockspaceId = 1337;
+ private const float _tableWidth = 17;
public SnimGui(int width, int height)
{
@@ -300,11 +300,12 @@ Snooper aims to give an accurate preview of models, materials, skeletal animatio
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero);
Window("Outliner", () =>
{
- if (ImGui.BeginTable("Items", 3, ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersOuterV, ImGui.GetContentRegionAvail()))
+ if (ImGui.BeginTable("Items", 4, ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersOuterV | ImGuiTableFlags.NoSavedSettings, ImGui.GetContentRegionAvail()))
{
- ImGui.TableSetupColumn("Instance", ImGuiTableColumnFlags.NoHeaderWidth | ImGuiTableColumnFlags.WidthFixed);
- ImGui.TableSetupColumn("Channels", ImGuiTableColumnFlags.WidthFixed);
- ImGui.TableSetupColumn("Name");
+ ImGui.TableSetupColumn("Instance", ImGuiTableColumnFlags.NoHeaderWidth | ImGuiTableColumnFlags.WidthFixed, _tableWidth);
+ ImGui.TableSetupColumn("Channels", ImGuiTableColumnFlags.NoHeaderWidth | ImGuiTableColumnFlags.WidthFixed, _tableWidth);
+ ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthStretch);
+ ImGui.TableSetupColumn("", ImGuiTableColumnFlags.NoHeaderWidth | ImGuiTableColumnFlags.WidthFixed, _tableWidth);
ImGui.TableHeadersRow();
var i = 0;
@@ -314,9 +315,9 @@ Snooper aims to give an accurate preview of models, materials, skeletal animatio
ImGui.TableNextRow();
ImGui.TableNextColumn();
if (!model.Show)
- {
ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, ImGui.GetColorU32(new Vector4(1, 0, 0, .5f)));
- }
+ if (model.IsAttached)
+ ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, ImGui.GetColorU32(new Vector4(1, 1, 0, .5f)));
ImGui.Text(model.TransformsCount.ToString("D"));
ImGui.TableNextColumn();
@@ -326,7 +327,6 @@ Snooper aims to give an accurate preview of models, materials, skeletal animatio
{
s.Renderer.Options.SelectModel(guid);
}
-
Popup(() =>
{
s.Renderer.Options.SelectModel(guid);
@@ -358,6 +358,11 @@ Snooper aims to give an accurate preview of models, materials, skeletal animatio
ImGui.Separator();
if (ImGui.Selectable("Copy Name to Clipboard")) ImGui.SetClipboardText(model.Name);
});
+
+ ImGui.TableNextColumn();
+ ImGui.Image(s.Renderer.Options.Icons[model.IsAttached ? "link_on" : "link_off"].GetPointer(), new Vector2(_tableWidth));
+ if (model.IsAttached) TooltipCopy($"Attached To {model.AttachedTo}");
+
ImGui.PopID();
i++;
}
@@ -411,15 +416,11 @@ Snooper aims to give an accurate preview of models, materials, skeletal animatio
{
case false when ImGui.Button($"Attach to '{socket.Name}'"):
socket.AttachedModels.Add(selectedGuid);
- // reset PRS to 0 so it's attached to the actual position (can be transformed relative to the socket later by the user)
- selectedModel.Transforms[selectedModel.SelectedInstance].Position = FVector.ZeroVector;
- selectedModel.Transforms[selectedModel.SelectedInstance].Rotation = FQuat.Identity;
- selectedModel.Transforms[selectedModel.SelectedInstance].Scale = FVector.OneVector;
+ selectedModel.AttachModel(model, socket);
break;
case true when ImGui.Button($"Detach from '{socket.Name}'"):
socket.AttachedModels.Remove(selectedGuid);
- // reset PRS relation to O
- selectedModel.Transforms[selectedModel.SelectedInstance].Relation = Matrix4x4.Identity;
+ selectedModel.DetachModel();
break;
}
ImGui.PopID();
@@ -451,26 +452,26 @@ Snooper aims to give an accurate preview of models, materials, skeletal animatio
{
if (ImGui.BeginTable("model_details", 2, ImGuiTableFlags.SizingStretchProp))
{
- Layout("Entity");ImGui.Text($" : ({model.Type}) {model.Name}");
- Layout("Guid");ImGui.Text($" : {s.Renderer.Options.SelectedModel.ToString(EGuidFormats.UniqueObjectGuid)}");
+ Layout("Entity");ImGui.Text($" : ({model.Type}) {model.Name}");
+ Layout("Guid");ImGui.Text($" : {s.Renderer.Options.SelectedModel.ToString(EGuidFormats.UniqueObjectGuid)}");
if (model.HasSkeleton)
{
- Layout("Skeleton");ImGui.Text($" : {model.Skeleton.UnrealSkeleton.Name}");
- Layout("Bones");ImGui.Text($" : x{model.Skeleton.UnrealSkeleton.BoneTree.Length}");
+ Layout("Skeleton");ImGui.Text($" : {model.Skeleton.UnrealSkeleton.Name}");
+ Layout("Bones");ImGui.Text($" : x{model.Skeleton.UnrealSkeleton.BoneTree.Length}");
}
else
{
- Layout("Two Sided");ImGui.Text($" : {model.TwoSided}");
+ Layout("Two Sided");ImGui.Text($" : {model.TwoSided}");
}
- Layout("Sockets");ImGui.Text($" : x{model.Sockets.Length}");
+ Layout("Sockets");ImGui.Text($" : x{model.Sockets.Length}");
ImGui.EndTable();
}
if (ImGui.BeginTabBar("tabbar_details", ImGuiTabBarFlags.None))
{
- if (ImGui.BeginTabItem("Sections") && ImGui.BeginTable("table_sections", 2, ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersOuterV, ImGui.GetContentRegionAvail()))
+ if (ImGui.BeginTabItem("Sections") && ImGui.BeginTable("table_sections", 2, ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersOuterV | ImGuiTableFlags.NoSavedSettings, ImGui.GetContentRegionAvail()))
{
- ImGui.TableSetupColumn("Index", ImGuiTableColumnFlags.WidthFixed);
+ ImGui.TableSetupColumn("Index", ImGuiTableColumnFlags.NoHeaderWidth | ImGuiTableColumnFlags.WidthFixed, _tableWidth);
ImGui.TableSetupColumn("Material");
ImGui.TableHeadersRow();
diff --git a/FModel/Views/Snooper/Transform.cs b/FModel/Views/Snooper/Transform.cs
index fc66d2e6..8a623853 100644
--- a/FModel/Views/Snooper/Transform.cs
+++ b/FModel/Views/Snooper/Transform.cs
@@ -13,7 +13,7 @@ public class Transform
public Matrix4x4 Relation = Matrix4x4.Identity;
public FVector Position = FVector.ZeroVector;
- public FQuat Rotation = new (0f);
+ public FQuat Rotation = FQuat.Identity;
public FVector Scale = FVector.OneVector;
public Matrix4x4 Matrix =>