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