From 5b5dd8be53ae4fa2b7ae48067f53bb051c2533e1 Mon Sep 17 00:00:00 2001 From: 4sval Date: Thu, 16 Feb 2023 19:47:19 +0100 Subject: [PATCH] timeline is world relative - part1 --- FModel/MainWindow.xaml.cs | 4 +- FModel/Views/Snooper/Animations/Animation.cs | 75 +++++ .../{Models => }/Animations/BoneIndice.cs | 2 +- FModel/Views/Snooper/Animations/Sequence.cs | 40 +++ FModel/Views/Snooper/Animations/Skeleton.cs | 270 ++++++++++++++++++ .../Views/Snooper/Animations/TimeTracker.cs | 134 +++++++++ .../Snooper/Models/Animations/Animation.cs | 183 ------------ .../Snooper/Models/Animations/Sequence.cs | 146 ---------- .../Snooper/Models/Animations/Skeleton.cs | 166 ----------- FModel/Views/Snooper/Models/Cube.cs | 3 +- FModel/Views/Snooper/Models/Model.cs | 40 +-- FModel/Views/Snooper/Options.cs | 23 +- FModel/Views/Snooper/Renderer.cs | 62 ++-- FModel/Views/Snooper/SnimGui.cs | 20 +- 14 files changed, 618 insertions(+), 550 deletions(-) create mode 100644 FModel/Views/Snooper/Animations/Animation.cs rename FModel/Views/Snooper/{Models => }/Animations/BoneIndice.cs (89%) create mode 100644 FModel/Views/Snooper/Animations/Sequence.cs create mode 100644 FModel/Views/Snooper/Animations/Skeleton.cs create mode 100644 FModel/Views/Snooper/Animations/TimeTracker.cs delete mode 100644 FModel/Views/Snooper/Models/Animations/Animation.cs delete mode 100644 FModel/Views/Snooper/Models/Animations/Sequence.cs delete mode 100644 FModel/Views/Snooper/Models/Animations/Skeleton.cs diff --git a/FModel/MainWindow.xaml.cs b/FModel/MainWindow.xaml.cs index 0d6b8be2..f5214a29 100644 --- a/FModel/MainWindow.xaml.cs +++ b/FModel/MainWindow.xaml.cs @@ -77,10 +77,10 @@ public partial class MainWindow #if DEBUG await _threadWorkerView.Begin(cancellationToken => _applicationView.CUE4Parse.Extract(cancellationToken, - "fortnitegame/Content/Characters/Player/Female/Medium/Heads/F_MED_HIS_Ramirez_Head_01/Mesh/F_MED_HIS_Ramirez_Head_01.uasset")); + "fortnitegame/Content/Characters/Player/Male/Medium/Bodies/M_Med_Soldier_04/Meshes/SK_M_Med_Soldier_04.uasset")); await _threadWorkerView.Begin(cancellationToken => _applicationView.CUE4Parse.Extract(cancellationToken, - "fortnitegame/Content/Animation/Game/MainPlayer/Emotes/Calculated/Emote_Calculated_CMF_Montage.uasset")); + "fortnitegame/Content/Animation/Game/MainPlayer/Emotes/Basketball_Tricks/Basketball_Tricks_Loop_CMM_M.uasset")); #endif } diff --git a/FModel/Views/Snooper/Animations/Animation.cs b/FModel/Views/Snooper/Animations/Animation.cs new file mode 100644 index 00000000..93650eb5 --- /dev/null +++ b/FModel/Views/Snooper/Animations/Animation.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using CUE4Parse_Conversion.Animations; +using CUE4Parse.UE4.Objects.Core.Misc; +using CUE4Parse.Utils; + +namespace FModel.Views.Snooper.Animations; + +public class Animation : IDisposable +{ + public readonly Sequence[] Sequences; + public readonly float EndTime; // Animation End Time + public readonly float TotalElapsedTime; // Animation Max Time + + public int CurrentSequence; + public int FrameInSequence; // Current Sequence's Frame to Display + + public readonly List AttachedModels; + + public Animation() + { + Sequences = Array.Empty(); + AttachedModels = new List(); + } + + public Animation(CAnimSet animSet) : this() + { + Sequences = new Sequence[animSet.Sequences.Count]; + for (int i = 0; i < Sequences.Length; i++) + { + Sequences[i] = new Sequence(animSet.Sequences[i]); + + TotalElapsedTime += animSet.Sequences[i].NumFrames * Sequences[i].TimePerFrame; + EndTime = Sequences[i].EndTime; + } + + // if (Sequences.Length > 0) + // Tracker.ElapsedTime = Sequences[0].StartTime; + } + + public Animation(CAnimSet animSet, params FGuid[] animatedModels) : this(animSet) + { + AttachedModels.AddRange(animatedModels); + } + + public void TimeCalculation(float elapsedTime) + { + for (int i = 0; i < Sequences.Length; i++) + { + if (elapsedTime < Sequences[i].EndTime && elapsedTime >= Sequences[i].StartTime) + { + CurrentSequence = i; + break; + } + } + if (elapsedTime >= TotalElapsedTime) Reset(); + + var lastEndTime = 0.0f; + for (int s = 0; s < CurrentSequence; s++) + lastEndTime = Sequences[s].EndTime; + + FrameInSequence = Math.Min(((elapsedTime - lastEndTime) / Sequences[CurrentSequence].TimePerFrame).FloorToInt(), Sequences[CurrentSequence].EndFrame); + } + + private void Reset() + { + FrameInSequence = 0; + CurrentSequence = 0; + } + + public void Dispose() + { + AttachedModels.Clear(); + } +} diff --git a/FModel/Views/Snooper/Models/Animations/BoneIndice.cs b/FModel/Views/Snooper/Animations/BoneIndice.cs similarity index 89% rename from FModel/Views/Snooper/Models/Animations/BoneIndice.cs rename to FModel/Views/Snooper/Animations/BoneIndice.cs index b4094bf7..25c7331f 100644 --- a/FModel/Views/Snooper/Models/Animations/BoneIndice.cs +++ b/FModel/Views/Snooper/Animations/BoneIndice.cs @@ -1,4 +1,4 @@ -namespace FModel.Views.Snooper.Models.Animations; +namespace FModel.Views.Snooper.Animations; public class BoneIndice { diff --git a/FModel/Views/Snooper/Animations/Sequence.cs b/FModel/Views/Snooper/Animations/Sequence.cs new file mode 100644 index 00000000..ed73fa4e --- /dev/null +++ b/FModel/Views/Snooper/Animations/Sequence.cs @@ -0,0 +1,40 @@ +using System.Numerics; +using CUE4Parse_Conversion.Animations; +using CUE4Parse.Utils; +using ImGuiNET; + +namespace FModel.Views.Snooper.Animations; + +public class Sequence +{ + public readonly string Name; + public readonly float TimePerFrame; + public readonly float StartTime; + public readonly float Duration; + public readonly float EndTime; + public readonly int EndFrame; + public readonly int LoopingCount; + + public Sequence(CAnimSequence sequence) + { + Name = sequence.Name; + TimePerFrame = 1.0f / sequence.Rate; + StartTime = sequence.StartPos; + Duration = sequence.AnimEndTime; + EndTime = StartTime + Duration; + EndFrame = (Duration / TimePerFrame).FloorToInt() - 1; + LoopingCount = sequence.LoopingCount; + } + + private readonly float _height = 20.0f; + public void DrawSequence(ImDrawListPtr drawList, float x, float y, Vector2 ratio, int index, uint col) + { + var height = _height * (index % 2); + var p1 = new Vector2(x + StartTime * ratio.X, y + height); + var p2 = new Vector2(x + EndTime * ratio.X, y + height + _height); + drawList.PushClipRect(p1, p2, true); + drawList.AddRectFilled(p1, p2, col); + drawList.AddText(p1 with { X = p1.X + 2.5f }, 0xFF000000, Name); + drawList.PopClipRect(); + } +} diff --git a/FModel/Views/Snooper/Animations/Skeleton.cs b/FModel/Views/Snooper/Animations/Skeleton.cs new file mode 100644 index 00000000..edffb7e1 --- /dev/null +++ b/FModel/Views/Snooper/Animations/Skeleton.cs @@ -0,0 +1,270 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using CUE4Parse_Conversion.Animations; +using CUE4Parse.UE4.Assets.Exports.Animation; +using CUE4Parse.UE4.Objects.Core.Math; +using FModel.Views.Snooper.Buffers; +using OpenTK.Graphics.OpenGL4; +using Serilog; + +namespace FModel.Views.Snooper.Animations; + +public class Skeleton : IDisposable +{ + private int _handle; + private BufferObject _ssbo; + + public string Name; + public readonly Dictionary BonesIndicesByLoweredName; + public readonly Dictionary BonesTransformByIndex; + + private int _previousAnimationSequence; + private int _previousSequenceFrame; + private Transform[][][] _animatedBonesTransform; // [sequence][bone][frame] + private readonly Matrix4x4[] _invertedBonesMatrix; + public int BoneCount => _invertedBonesMatrix.Length; + public bool IsAnimated => _animatedBonesTransform.Length > 0; + + public Skeleton() + { + BonesIndicesByLoweredName = new Dictionary(); + BonesTransformByIndex = new Dictionary(); + _animatedBonesTransform = Array.Empty(); + _invertedBonesMatrix = Array.Empty(); + } + + public Skeleton(FReferenceSkeleton referenceSkeleton) : this() + { + for (int boneIndex = 0; boneIndex < referenceSkeleton.FinalRefBoneInfo.Length; boneIndex++) + { + var info = referenceSkeleton.FinalRefBoneInfo[boneIndex]; + + var boneIndices = new BoneIndice { BoneIndex = boneIndex, ParentBoneIndex = info.ParentIndex }; + if (!boneIndices.IsRoot) + boneIndices.LoweredParentBoneName = + referenceSkeleton.FinalRefBoneInfo[boneIndices.ParentBoneIndex].Name.Text.ToLower(); + + BonesIndicesByLoweredName[info.Name.Text.ToLower()] = boneIndices; + } + + _invertedBonesMatrix = new Matrix4x4[BonesIndicesByLoweredName.Count]; + foreach (var boneIndices in BonesIndicesByLoweredName.Values) + { + var bone = referenceSkeleton.FinalRefBonePose[boneIndices.BoneIndex]; + if (!BonesTransformByIndex.TryGetValue(boneIndices.BoneIndex, out var boneTransform)) + { + boneTransform = new Transform + { + Rotation = bone.Rotation, + Position = bone.Translation * Constants.SCALE_DOWN_RATIO, + Scale = bone.Scale3D + }; + } + + if (!BonesTransformByIndex.TryGetValue(boneIndices.ParentBoneIndex, out var parentTransform)) + parentTransform = new Transform { Relation = Matrix4x4.Identity }; + + boneTransform.Relation = parentTransform.Matrix; + Matrix4x4.Invert(boneTransform.Matrix, out var inverted); + + + BonesTransformByIndex[boneIndices.BoneIndex] = boneTransform; + _invertedBonesMatrix[boneIndices.BoneIndex] = inverted; + } + } + + public void Animate(CAnimSet anim, bool rotationOnly) + { + TrackSkeleton(anim); + + _animatedBonesTransform = new Transform[anim.Sequences.Count][][]; + for (int s = 0; s < _animatedBonesTransform.Length; s++) + { + var sequence = anim.Sequences[s]; + _animatedBonesTransform[s] = new Transform[BoneCount][]; + foreach (var boneIndices in BonesIndicesByLoweredName.Values) + { + var originalTransform = BonesTransformByIndex[boneIndices.BoneIndex]; + _animatedBonesTransform[s][boneIndices.BoneIndex] = new Transform[sequence.NumFrames]; + + if (!boneIndices.HasTrack) + { + for (int frame = 0; frame < _animatedBonesTransform[s][boneIndices.BoneIndex].Length; frame++) + { + _animatedBonesTransform[s][boneIndices.BoneIndex][frame] = new Transform + { + Relation = originalTransform.LocalMatrix * _animatedBonesTransform[s][boneIndices.ParentTrackIndex][frame].Matrix + }; + } + } + else + { + var trackIndex = boneIndices.TrackIndex; + for (int frame = 0; frame < _animatedBonesTransform[s][boneIndices.BoneIndex].Length; frame++) + { + var boneOrientation = originalTransform.Rotation; + var bonePosition = originalTransform.Position; + var boneScale = originalTransform.Scale; + + sequence.Tracks[trackIndex].GetBonePosition(frame, sequence.NumFrames, false, ref bonePosition, ref boneOrientation); + if (frame < sequence.Tracks[trackIndex].KeyScale.Length) + boneScale = sequence.Tracks[trackIndex].KeyScale[frame]; + + switch (anim.BoneModes[trackIndex]) + { + case EBoneTranslationRetargetingMode.Skeleton when !rotationOnly: + { + var targetTransform = sequence.RetargetBasePose?[trackIndex] ?? anim.BonePositions[trackIndex]; + bonePosition = targetTransform.Translation; + break; + } + case EBoneTranslationRetargetingMode.AnimationScaled when !rotationOnly: + { + var sourceTranslationLength = (originalTransform.Position / Constants.SCALE_DOWN_RATIO).Size(); + if (sourceTranslationLength > UnrealMath.KindaSmallNumber) + { + var targetTranslationLength = sequence.RetargetBasePose?[trackIndex].Translation.Size() ?? anim.BonePositions[trackIndex].Translation.Size(); + bonePosition.Scale(targetTranslationLength / sourceTranslationLength); + } + break; + } + case EBoneTranslationRetargetingMode.AnimationRelative when !rotationOnly: + { + // https://github.com/EpicGames/UnrealEngine/blob/cdaec5b33ea5d332e51eee4e4866495c90442122/Engine/Source/Runtime/Engine/Private/Animation/AnimationRuntime.cpp#L2586 + var refPoseTransform = sequence.RetargetBasePose?[trackIndex] ?? anim.BonePositions[trackIndex]; + break; + } + case EBoneTranslationRetargetingMode.OrientAndScale when !rotationOnly: + { + var sourceSkelTrans = originalTransform.Position / Constants.SCALE_DOWN_RATIO; + var targetSkelTrans = sequence.RetargetBasePose?[trackIndex].Translation ?? anim.BonePositions[trackIndex].Translation; + + if (!sourceSkelTrans.Equals(targetSkelTrans)) + { + var sourceSkelTransLength = sourceSkelTrans.Size(); + var targetSkelTransLength = targetSkelTrans.Size(); + if (!UnrealMath.IsNearlyZero(sourceSkelTransLength * targetSkelTransLength)) + { + var sourceSkelTransDir = sourceSkelTrans / sourceSkelTransLength; + var targetSkelTransDir = targetSkelTrans / targetSkelTransLength; + + var deltaRotation = FQuat.FindBetweenNormals(sourceSkelTransDir, targetSkelTransDir); + var scale = targetSkelTransLength / sourceSkelTransLength; + bonePosition = deltaRotation.RotateVector(bonePosition) * scale; + } + } + break; + } + } + + // revert FixRotationKeys + if (trackIndex > 0) boneOrientation.Conjugate(); + bonePosition *= Constants.SCALE_DOWN_RATIO; + + _animatedBonesTransform[s][boneIndices.BoneIndex][frame] = new Transform + { + Relation = boneIndices.HasParentTrack ? _animatedBonesTransform[s][boneIndices.ParentTrackIndex][frame].Matrix : originalTransform.Relation, + Rotation = boneOrientation, + Position = rotationOnly ? originalTransform.Position : bonePosition, + Scale = boneScale + }; + } + } + } + } + } + + private void TrackSkeleton(CAnimSet anim) + { + // reset + foreach (var boneIndices in BonesIndicesByLoweredName.Values) + { + boneIndices.TrackIndex = -1; + boneIndices.ParentTrackIndex = -1; + } + + // tracked bones + for (int trackIndex = 0; trackIndex < anim.TrackBonesInfo.Length; trackIndex++) + { + var info = anim.TrackBonesInfo[trackIndex]; + if (!BonesIndicesByLoweredName.TryGetValue(info.Name.Text.ToLower(), out var boneIndices)) + continue; + + boneIndices.TrackIndex = trackIndex; + var parentTrackIndex = info.ParentIndex; + if (parentTrackIndex < 0) continue; + + do + { + info = anim.TrackBonesInfo[parentTrackIndex]; + if (BonesIndicesByLoweredName.TryGetValue(info.Name.Text.ToLower(), out var parentBoneIndices) && parentBoneIndices.HasTrack) + boneIndices.ParentTrackIndex = parentBoneIndices.BoneIndex; + else parentTrackIndex = info.ParentIndex; + } while (!boneIndices.HasParentTrack); + } + + // fix parent of untracked bones + foreach ((var boneName, var boneIndices) in BonesIndicesByLoweredName) + { + if (boneIndices.IsRoot || boneIndices.HasTrack && boneIndices.HasParentTrack) // assuming root bone always has a track + continue; + +#if DEBUG + Log.Warning($"Bone Mismatch: {boneName} ({boneIndices.BoneIndex}) was not present in the anim's target skeleton"); +#endif + + var loweredParentBoneName = boneIndices.LoweredParentBoneName; + do + { + var parentBoneIndices = BonesIndicesByLoweredName[loweredParentBoneName]; + if (parentBoneIndices.HasTrack) boneIndices.ParentTrackIndex = parentBoneIndices.BoneIndex; + else loweredParentBoneName = parentBoneIndices.LoweredParentBoneName; + } while (!boneIndices.HasParentTrack); + } + } + + public void Setup() + { + _handle = GL.CreateProgram(); + + _ssbo = new BufferObject(BoneCount, BufferTarget.ShaderStorageBuffer); + for (int boneIndex = 0; boneIndex < BoneCount; boneIndex++) + _ssbo.Update(boneIndex, Matrix4x4.Identity); + } + + public void UpdateAnimationMatrices(int currentSequence, int frameInSequence) + { + if (!IsAnimated) return; + + _previousAnimationSequence = currentSequence; + if (_previousSequenceFrame == frameInSequence) return; + _previousSequenceFrame = frameInSequence; + + _ssbo.Bind(); + for (int boneIndex = 0; boneIndex < BoneCount; boneIndex++) + _ssbo.Update(boneIndex, _invertedBonesMatrix[boneIndex] * _animatedBonesTransform[_previousAnimationSequence][boneIndex][_previousSequenceFrame].Matrix); + _ssbo.Unbind(); + } + + public Matrix4x4 GetBoneMatrix(BoneIndice boneIndices) + { + return IsAnimated + ? _animatedBonesTransform[_previousAnimationSequence][boneIndices.BoneIndex][_previousSequenceFrame].Matrix + : BonesTransformByIndex[boneIndices.BoneIndex].Matrix; + } + + public void Render() + { + _ssbo.BindBufferBase(1); + } + + public void Dispose() + { + BonesIndicesByLoweredName.Clear(); + BonesTransformByIndex.Clear(); + + _ssbo?.Dispose(); + GL.DeleteProgram(_handle); + } +} diff --git a/FModel/Views/Snooper/Animations/TimeTracker.cs b/FModel/Views/Snooper/Animations/TimeTracker.cs new file mode 100644 index 00000000..3b87a7aa --- /dev/null +++ b/FModel/Views/Snooper/Animations/TimeTracker.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using ImGuiNET; + +namespace FModel.Views.Snooper.Animations; + +public enum ETrackerType +{ + Start, + Frame, + InBetween, + End +} + +public class TimeTracker : IDisposable +{ + public bool IsPaused; + public float ElapsedTime; + public float MaxElapsedTime { get; private set; } + + private float _timeHeight = 10.0f; + private float _timeBarHeight => _timeHeight * 2.0f; + + public TimeTracker() + { + Reset(); + SetMaxElapsedTime(0.01f); + } + + public void Update(float deltaSeconds) + { + if (IsPaused) return; + ElapsedTime += deltaSeconds; + if (ElapsedTime >= MaxElapsedTime) Reset(); + } + + public void SetMaxElapsedTime(float maxElapsedTime) + { + MaxElapsedTime = MathF.Max(maxElapsedTime, MaxElapsedTime); + } + + public void Reset() + { + IsPaused = false; + ElapsedTime = 0.0f; + } + + public void Dispose() + { + Reset(); + } + + public void ImGuiTimeline(ImFontPtr fontPtr, List animations) + { + var io = ImGui.GetIO(); + var canvasP0 = ImGui.GetCursorScreenPos(); + var canvasSize = ImGui.GetContentRegionAvail(); + var canvasP1 = new Vector2(canvasP0.X + canvasSize.X, canvasP0.Y + canvasSize.Y); + var timeRatio = canvasSize / MaxElapsedTime; + + var drawList = ImGui.GetWindowDrawList(); + + ImGui.InvisibleButton("timeline_canvas", canvasP1 with { Y = _timeBarHeight }, ImGuiButtonFlags.MouseButtonLeft); + IsPaused = ImGui.IsItemActive(); + if (IsPaused && ImGui.IsMouseDragging(ImGuiMouseButton.Left)) + { + var mousePosCanvas = io.MousePos - canvasP0; + ElapsedTime = Math.Clamp(mousePosCanvas.X / canvasSize.X * MaxElapsedTime, 0.01f, MaxElapsedTime); + foreach (var animation in animations) + { + animation.TimeCalculation(ElapsedTime); + } + } + + drawList.AddRectFilled(canvasP0, canvasP1 with { Y = canvasP0.Y + _timeBarHeight }, 0xFF181818); + drawList.PushClipRect(canvasP0, canvasP1 with { Y = canvasP0.Y + _timeBarHeight }, true); + { + for (float x = 0; x < canvasSize.X; x += timeRatio.X * MaxElapsedTime / canvasSize.X * 50.0f) + { + drawList.AddLine(new Vector2(canvasP0.X + x, canvasP0.Y + _timeHeight + 2.5f), canvasP1 with { X = canvasP0.X + x }, 0xA0FFFFFF); + drawList.AddText(fontPtr, 14, new Vector2(canvasP0.X + x + 4, canvasP0.Y + 7.5f), 0x50FFFFFF, $"{x / timeRatio.X:F1}s"); + } + } + drawList.PopClipRect(); + + // for (int i = 0; i < Sequences.Length; i++) + // { + // Sequences[i].DrawSequence(drawList, canvasP0.X, canvasP0.Y + _timeBarHeight, timeRatio, i, i == CurrentSequence ? 0xFF0000FF : 0xFF175F17); + // } + + DrawSeparator(drawList, canvasP0, canvasP1, ElapsedTime * timeRatio.X, ETrackerType.Frame); + // DrawSeparator(drawList, canvasP0, canvasP1, EndTime * timeRatio.X, ETrackerType.End); + } + + private void DrawSeparator(ImDrawListPtr drawList, Vector2 origin, Vector2 destination, float time, ETrackerType separatorType) + { + const int size = 5; + + Vector2 p1 = separatorType switch + { + ETrackerType.Frame => new Vector2(origin.X + time, origin.Y + _timeBarHeight), + ETrackerType.End => origin with { X = origin.X + time }, + _ => throw new ArgumentOutOfRangeException(nameof(separatorType), separatorType, null) + }; + var p2 = new Vector2(p1.X, destination.Y); + + uint color = separatorType switch + { + ETrackerType.Frame => 0xFF6F6F6F, + ETrackerType.End => 0xFF2E3E82, + _ => throw new ArgumentOutOfRangeException(nameof(separatorType), separatorType, null) + }; + + drawList.AddLine(p1, p2, color, 1f); + switch (separatorType) + { + case ETrackerType.Frame: + color = 0xFF30478C; + var xl = p1.X - size; + var xr = p1.X + size; + var yb = origin.Y + _timeBarHeight - _timeHeight / 2.0f; + + drawList.AddQuadFilled(origin with { X = xl }, origin with { X = xr }, new Vector2(xr, yb), new Vector2(xl, yb), color); + drawList.AddTriangleFilled(new Vector2(xl, yb), new Vector2(xr, yb), p1, color); + break; + case ETrackerType.End: + drawList.AddTriangleFilled(p1, p1 with { X = p1.X - size }, p1 with { Y = p1.Y + size }, color); + break; + default: + throw new ArgumentOutOfRangeException(nameof(separatorType), separatorType, null); + } + } +} diff --git a/FModel/Views/Snooper/Models/Animations/Animation.cs b/FModel/Views/Snooper/Models/Animations/Animation.cs deleted file mode 100644 index 8d273dca..00000000 --- a/FModel/Views/Snooper/Models/Animations/Animation.cs +++ /dev/null @@ -1,183 +0,0 @@ -using System; -using System.Numerics; -using CUE4Parse_Conversion.Animations; -using CUE4Parse.Utils; -using ImGuiNET; - -namespace FModel.Views.Snooper.Models.Animations; - -public enum AnimSeparatorType -{ - Start, - Frame, - InBetween, - End -} - -public class Animation : IDisposable -{ - public float ElapsedTime; // Animation Elapsed Time - public int FrameInSequence; // Current Sequence's Frame to Display - - public bool IsPaused; - public readonly float EndTime; // Animation End Time - public readonly float TotalElapsedTime; // Animation Max Time - - public int CurrentSequence; - public readonly Sequence[] Sequences; - public int SequencesCount => Sequences.Length; - - public Animation() - { - Reset(); - - IsPaused = false; - EndTime = 0.0f; - TotalElapsedTime = 0.0f; - Sequences = Array.Empty(); - } - - public Animation(Skeleton skeleton, CAnimSet anim, bool rotationOnly) : this() - { - Sequences = new Sequence[anim.Sequences.Count]; - for (int i = 0; i < Sequences.Length; i++) - { - Sequences[i] = new Sequence(skeleton, anim, anim.Sequences[i], rotationOnly); - - TotalElapsedTime += anim.Sequences[i].NumFrames * Sequences[i].TimePerFrame; - EndTime = Sequences[i].EndTime; - } - - if (Sequences.Length > 0) - ElapsedTime = Sequences[0].StartTime; - } - - public void Update(float deltaSeconds) - { - if (IsPaused) return; - - ElapsedTime += deltaSeconds; - TimeCalculation(); - } - - public Matrix4x4 InterpolateBoneTransform(int boneIndex) - { - // interpolate here - return Sequences[CurrentSequence].BonesTransform[boneIndex][FrameInSequence].Matrix; - } - - private void TimeCalculation() - { - for (int i = 0; i < Sequences.Length; i++) - { - if (ElapsedTime < Sequences[i].EndTime && ElapsedTime >= Sequences[i].StartTime) - { - CurrentSequence = i; - break; - } - } - if (ElapsedTime >= TotalElapsedTime) Reset(); - - var lastEndTime = 0.0f; - for (int s = 0; s < CurrentSequence; s++) - lastEndTime = Sequences[s].EndTime; - - FrameInSequence = Math.Min(((ElapsedTime - lastEndTime) / Sequences[CurrentSequence].TimePerFrame).FloorToInt(), Sequences[CurrentSequence].EndFrame); - } - - private void Reset() - { - ElapsedTime = 0.0f; - FrameInSequence = 0; - CurrentSequence = 0; - } - - public void Dispose() - { - Reset(); - for (int i = 0; i < Sequences.Length; i++) - { - Sequences[i]?.Dispose(); - } - } - - private float _timeHeight = 10.0f; - private float _timeBarHeight => _timeHeight * 2.0f; - public void ImGuiTimeline(ImFontPtr fontPtr) - { - var io = ImGui.GetIO(); - var canvasP0 = ImGui.GetCursorScreenPos(); - var canvasSize = ImGui.GetContentRegionAvail(); - var canvasP1 = new Vector2(canvasP0.X + canvasSize.X, canvasP0.Y + canvasSize.Y); - var timeRatio = canvasSize / TotalElapsedTime; - - var drawList = ImGui.GetWindowDrawList(); - - ImGui.InvisibleButton("timeline_canvas", canvasP1 with { Y = _timeBarHeight }, ImGuiButtonFlags.MouseButtonLeft); - IsPaused = ImGui.IsItemActive(); - if (IsPaused && ImGui.IsMouseDragging(ImGuiMouseButton.Left)) - { - var mousePosCanvas = io.MousePos - canvasP0; - ElapsedTime = Math.Clamp(mousePosCanvas.X / canvasSize.X * TotalElapsedTime, 0, TotalElapsedTime); - TimeCalculation(); - } - - drawList.AddRectFilled(canvasP0, canvasP1 with { Y = canvasP0.Y + _timeBarHeight }, 0xFF181818); - drawList.PushClipRect(canvasP0, canvasP1 with { Y = canvasP0.Y + _timeBarHeight }, true); - { - for (float x = 0; x < canvasSize.X; x += timeRatio.X * TotalElapsedTime / canvasSize.X * 50.0f) - { - drawList.AddLine(new Vector2(canvasP0.X + x, canvasP0.Y + _timeHeight + 2.5f), canvasP1 with { X = canvasP0.X + x }, 0xA0FFFFFF); - drawList.AddText(fontPtr, 14, new Vector2(canvasP0.X + x + 4, canvasP0.Y + 7.5f), 0x50FFFFFF, $"{x / timeRatio.X:F1}s"); - } - } - drawList.PopClipRect(); - - for (int i = 0; i < Sequences.Length; i++) - { - Sequences[i].DrawSequence(drawList, canvasP0.X, canvasP0.Y + _timeBarHeight, timeRatio, i, i == CurrentSequence ? 0xFF0000FF : 0xFF175F17); - } - - DrawSeparator(drawList, canvasP0, canvasP1, ElapsedTime * timeRatio.X, AnimSeparatorType.Frame); - DrawSeparator(drawList, canvasP0, canvasP1, EndTime * timeRatio.X, AnimSeparatorType.End); - } - - private void DrawSeparator(ImDrawListPtr drawList, Vector2 origin, Vector2 destination, float time, AnimSeparatorType separatorType) - { - const int size = 5; - - Vector2 p1 = separatorType switch - { - AnimSeparatorType.Frame => new Vector2(origin.X + time, origin.Y + _timeBarHeight), - AnimSeparatorType.End => origin with { X = origin.X + time }, - _ => throw new ArgumentOutOfRangeException(nameof(separatorType), separatorType, null) - }; - var p2 = new Vector2(p1.X, destination.Y); - - uint color = separatorType switch - { - AnimSeparatorType.Frame => 0xFF6F6F6F, - AnimSeparatorType.End => 0xFF2E3E82, - _ => throw new ArgumentOutOfRangeException(nameof(separatorType), separatorType, null) - }; - - drawList.AddLine(p1, p2, color, 1f); - switch (separatorType) - { - case AnimSeparatorType.Frame: - color = 0xFF30478C; - var xl = p1.X - size; - var xr = p1.X + size; - var yb = origin.Y + _timeBarHeight - _timeHeight / 2.0f; - - drawList.AddQuadFilled(origin with { X = xl }, origin with { X = xr }, new Vector2(xr, yb), new Vector2(xl, yb), color); - drawList.AddTriangleFilled(new Vector2(xl, yb), new Vector2(xr, yb), p1, color); - break; - case AnimSeparatorType.End: - drawList.AddTriangleFilled(p1, p1 with { X = p1.X - size }, p1 with { Y = p1.Y + size }, color); - break; - default: - throw new ArgumentOutOfRangeException(nameof(separatorType), separatorType, null); - } - } -} diff --git a/FModel/Views/Snooper/Models/Animations/Sequence.cs b/FModel/Views/Snooper/Models/Animations/Sequence.cs deleted file mode 100644 index 0fd08fb4..00000000 --- a/FModel/Views/Snooper/Models/Animations/Sequence.cs +++ /dev/null @@ -1,146 +0,0 @@ -using System; -using System.Linq; -using System.Numerics; -using CUE4Parse_Conversion.Animations; -using CUE4Parse.UE4.Assets.Exports.Animation; -using CUE4Parse.UE4.Objects.Core.Math; -using CUE4Parse.Utils; -using ImGuiNET; -using Serilog; - -namespace FModel.Views.Snooper.Models.Animations; - -public class Sequence : IDisposable -{ - public readonly string Name; - public readonly float TimePerFrame; - public readonly float StartTime; - public readonly float Duration; - public readonly float EndTime; - public readonly int EndFrame; - public readonly int LoopingCount; - - public readonly Transform[][] BonesTransform; - - private Sequence(CAnimSequence sequence) - { - Name = sequence.Name; - TimePerFrame = 1.0f / sequence.Rate; - StartTime = sequence.StartPos; - Duration = sequence.AnimEndTime; - EndTime = StartTime + Duration; - EndFrame = (Duration / TimePerFrame).FloorToInt() - 1; - LoopingCount = sequence.LoopingCount; - } - - public Sequence(Skeleton skeleton, CAnimSet anim, CAnimSequence sequence, bool rotationOnly) : this(sequence) - { - BonesTransform = new Transform[skeleton.BoneCount][]; - foreach (var boneIndices in skeleton.BonesIndicesByLoweredName.Values) - { - var originalTransform = skeleton.BonesTransformByIndex[boneIndices.BoneIndex]; - BonesTransform[boneIndices.BoneIndex] = new Transform[sequence.NumFrames]; - - if (!boneIndices.HasTrack) - { - for (int frame = 0; frame < BonesTransform[boneIndices.BoneIndex].Length; frame++) - { - BonesTransform[boneIndices.BoneIndex][frame] = new Transform - { - Relation = originalTransform.LocalMatrix * BonesTransform[boneIndices.ParentTrackIndex][frame].Matrix - }; - } - } - else - { - var trackIndex = boneIndices.TrackIndex; - for (int frame = 0; frame < BonesTransform[boneIndices.BoneIndex].Length; frame++) - { - var boneOrientation = originalTransform.Rotation; - var bonePosition = originalTransform.Position; - var boneScale = originalTransform.Scale; - - sequence.Tracks[trackIndex].GetBonePosition(frame, sequence.NumFrames, false, ref bonePosition, ref boneOrientation); - if (frame < sequence.Tracks[trackIndex].KeyScale.Length) - boneScale = sequence.Tracks[trackIndex].KeyScale[frame]; - - switch (anim.BoneModes[trackIndex]) - { - case EBoneTranslationRetargetingMode.Skeleton when !rotationOnly: - { - var targetTransform = sequence.RetargetBasePose?[trackIndex] ?? anim.BonePositions[trackIndex]; - bonePosition = targetTransform.Translation; - break; - } - case EBoneTranslationRetargetingMode.AnimationScaled when !rotationOnly: - { - var sourceTranslationLength = (originalTransform.Position / Constants.SCALE_DOWN_RATIO).Size(); - if (sourceTranslationLength > UnrealMath.KindaSmallNumber) - { - var targetTranslationLength = sequence.RetargetBasePose?[trackIndex].Translation.Size() ?? anim.BonePositions[trackIndex].Translation.Size(); - bonePosition.Scale(targetTranslationLength / sourceTranslationLength); - } - break; - } - case EBoneTranslationRetargetingMode.AnimationRelative when !rotationOnly: - { - // https://github.com/EpicGames/UnrealEngine/blob/cdaec5b33ea5d332e51eee4e4866495c90442122/Engine/Source/Runtime/Engine/Private/Animation/AnimationRuntime.cpp#L2586 - var refPoseTransform = sequence.RetargetBasePose?[trackIndex] ?? anim.BonePositions[trackIndex]; - break; - } - case EBoneTranslationRetargetingMode.OrientAndScale when !rotationOnly: - { - var sourceSkelTrans = originalTransform.Position / Constants.SCALE_DOWN_RATIO; - var targetSkelTrans = sequence.RetargetBasePose?[trackIndex].Translation ?? anim.BonePositions[trackIndex].Translation; - - if (!sourceSkelTrans.Equals(targetSkelTrans)) - { - var sourceSkelTransLength = sourceSkelTrans.Size(); - var targetSkelTransLength = targetSkelTrans.Size(); - if (!UnrealMath.IsNearlyZero(sourceSkelTransLength * targetSkelTransLength)) - { - var sourceSkelTransDir = sourceSkelTrans / sourceSkelTransLength; - var targetSkelTransDir = targetSkelTrans / targetSkelTransLength; - - var deltaRotation = FQuat.FindBetweenNormals(sourceSkelTransDir, targetSkelTransDir); - var scale = targetSkelTransLength / sourceSkelTransLength; - bonePosition = deltaRotation.RotateVector(bonePosition) * scale; - } - } - break; - } - } - - // revert FixRotationKeys - if (trackIndex > 0) boneOrientation.Conjugate(); - bonePosition *= Constants.SCALE_DOWN_RATIO; - - BonesTransform[boneIndices.BoneIndex][frame] = new Transform - { - Relation = boneIndices.HasParentTrack ? BonesTransform[boneIndices.ParentTrackIndex][frame].Matrix : originalTransform.Relation, - Rotation = boneOrientation, - Position = rotationOnly ? originalTransform.Position : bonePosition, - Scale = boneScale - }; - } - } - } - } - - public void Dispose() - { - - } - - private readonly float _height = 20.0f; - public void DrawSequence(ImDrawListPtr drawList, float x, float y, Vector2 ratio, int index, uint col) - { - var height = _height * (index % 2); - var p1 = new Vector2(x + StartTime * ratio.X, y + height); - var p2 = new Vector2(x + EndTime * ratio.X, y + height + _height); - drawList.PushClipRect(p1, p2, true); - drawList.AddRectFilled(p1, p2, col); - drawList.AddText(p1 with { X = p1.X + 2.5f }, 0xFF000000, Name); - drawList.PopClipRect(); - } -} diff --git a/FModel/Views/Snooper/Models/Animations/Skeleton.cs b/FModel/Views/Snooper/Models/Animations/Skeleton.cs deleted file mode 100644 index 73ce41f0..00000000 --- a/FModel/Views/Snooper/Models/Animations/Skeleton.cs +++ /dev/null @@ -1,166 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Numerics; -using CUE4Parse_Conversion.Animations; -using CUE4Parse.UE4.Assets.Exports.Animation; -using FModel.Views.Snooper.Buffers; -using OpenTK.Graphics.OpenGL4; -using Serilog; - -namespace FModel.Views.Snooper.Models.Animations; - -public class Skeleton : IDisposable -{ - private int _handle; - private BufferObject _ssbo; - - public string Name; - public readonly Dictionary BonesIndicesByLoweredName; - public readonly Dictionary BonesTransformByIndex; - public readonly Matrix4x4[] InvertedBonesMatrix; - public int BoneCount => InvertedBonesMatrix.Length; - - public Animation Anim; - public bool HasAnim => Anim != null; - - public Skeleton() - { - BonesIndicesByLoweredName = new Dictionary(); - BonesTransformByIndex = new Dictionary(); - InvertedBonesMatrix = Array.Empty(); - } - - public Skeleton(FReferenceSkeleton referenceSkeleton) : this() - { - for (int boneIndex = 0; boneIndex < referenceSkeleton.FinalRefBoneInfo.Length; boneIndex++) - { - var info = referenceSkeleton.FinalRefBoneInfo[boneIndex]; - - var boneIndices = new BoneIndice { BoneIndex = boneIndex, ParentBoneIndex = info.ParentIndex }; - if (!boneIndices.IsRoot) - boneIndices.LoweredParentBoneName = - referenceSkeleton.FinalRefBoneInfo[boneIndices.ParentBoneIndex].Name.Text.ToLower(); - - BonesIndicesByLoweredName[info.Name.Text.ToLower()] = boneIndices; - } - - InvertedBonesMatrix = new Matrix4x4[BonesIndicesByLoweredName.Count]; - foreach (var boneIndices in BonesIndicesByLoweredName.Values) - { - var bone = referenceSkeleton.FinalRefBonePose[boneIndices.BoneIndex]; - if (!BonesTransformByIndex.TryGetValue(boneIndices.BoneIndex, out var boneTransform)) - { - boneTransform = new Transform - { - Rotation = bone.Rotation, - Position = bone.Translation * Constants.SCALE_DOWN_RATIO, - Scale = bone.Scale3D - }; - } - - if (!BonesTransformByIndex.TryGetValue(boneIndices.ParentBoneIndex, out var parentTransform)) - parentTransform = new Transform { Relation = Matrix4x4.Identity }; - - boneTransform.Relation = parentTransform.Matrix; - Matrix4x4.Invert(boneTransform.Matrix, out var inverted); - - - BonesTransformByIndex[boneIndices.BoneIndex] = boneTransform; - InvertedBonesMatrix[boneIndices.BoneIndex] = inverted; - } - } - - public void SetAnimation(CAnimSet anim, bool rotationOnly) - { - TrackSkeleton(anim); - Anim = new Animation(this, anim, rotationOnly); - } - - private void TrackSkeleton(CAnimSet anim) - { - // reset - foreach (var boneIndices in BonesIndicesByLoweredName.Values) - { - boneIndices.TrackIndex = -1; - boneIndices.ParentTrackIndex = -1; - } - - // tracked bones - for (int trackIndex = 0; trackIndex < anim.TrackBonesInfo.Length; trackIndex++) - { - var info = anim.TrackBonesInfo[trackIndex]; - if (!BonesIndicesByLoweredName.TryGetValue(info.Name.Text.ToLower(), out var boneIndices)) - continue; - - boneIndices.TrackIndex = trackIndex; - var parentTrackIndex = info.ParentIndex; - if (parentTrackIndex < 0) continue; - - do - { - info = anim.TrackBonesInfo[parentTrackIndex]; - if (BonesIndicesByLoweredName.TryGetValue(info.Name.Text.ToLower(), out var parentBoneIndices) && parentBoneIndices.HasTrack) - boneIndices.ParentTrackIndex = parentBoneIndices.BoneIndex; - else parentTrackIndex = info.ParentIndex; - } while (!boneIndices.HasParentTrack); - } - - // fix parent of untracked bones - foreach ((var boneName, var boneIndices) in BonesIndicesByLoweredName) - { - if (boneIndices.IsRoot || boneIndices.HasTrack && boneIndices.HasParentTrack) // assuming root bone always has a track - continue; - -#if DEBUG - Log.Warning($"Bone Mismatch: {boneName} ({boneIndices.BoneIndex}) was not present in the anim's target skeleton"); -#endif - - var loweredParentBoneName = boneIndices.LoweredParentBoneName; - do - { - var parentBoneIndices = BonesIndicesByLoweredName[loweredParentBoneName]; - if (parentBoneIndices.HasTrack) boneIndices.ParentTrackIndex = parentBoneIndices.BoneIndex; - else loweredParentBoneName = parentBoneIndices.LoweredParentBoneName; - } while (!boneIndices.HasParentTrack); - } - } - - public void Setup() - { - _handle = GL.CreateProgram(); - - _ssbo = new BufferObject(BoneCount, BufferTarget.ShaderStorageBuffer); - for (int boneIndex = 0; boneIndex < BoneCount; boneIndex++) - _ssbo.Update(boneIndex, Matrix4x4.Identity); - } - - private int _previousFrame; - public void UpdateMatrices(float deltaSeconds) - { - if (!HasAnim) return; - - Anim.Update(deltaSeconds); - if (_previousFrame == Anim.FrameInSequence) return; - _previousFrame = Anim.FrameInSequence; - - _ssbo.Bind(); - for (int boneIndex = 0; boneIndex < BoneCount; boneIndex++) - _ssbo.Update(boneIndex, InvertedBonesMatrix[boneIndex] * Anim.InterpolateBoneTransform(boneIndex)); - _ssbo.Unbind(); - } - - public void Render() - { - _ssbo.BindBufferBase(1); - } - - public void Dispose() - { - BonesIndicesByLoweredName.Clear(); - BonesTransformByIndex.Clear(); - Anim?.Dispose(); - - _ssbo?.Dispose(); - GL.DeleteProgram(_handle); - } -} diff --git a/FModel/Views/Snooper/Models/Cube.cs b/FModel/Views/Snooper/Models/Cube.cs index 3681cf08..21f5f6f7 100644 --- a/FModel/Views/Snooper/Models/Cube.cs +++ b/FModel/Views/Snooper/Models/Cube.cs @@ -1,12 +1,13 @@ using CUE4Parse_Conversion.Meshes.PSK; using CUE4Parse.UE4.Assets.Exports.Material; +using CUE4Parse.UE4.Objects.Core.Misc; using FModel.Views.Snooper.Shading; namespace FModel.Views.Snooper.Models; public class Cube : Model { - public Cube(CStaticMesh mesh, UMaterialInterface unrealMaterial) : base(unrealMaterial) + public Cube(CStaticMesh mesh, FGuid guid, UMaterialInterface unrealMaterial) : base(unrealMaterial, guid) { var lod = mesh.LODs[0]; diff --git a/FModel/Views/Snooper/Models/Model.cs b/FModel/Views/Snooper/Models/Model.cs index 2fa29933..e6831d61 100644 --- a/FModel/Views/Snooper/Models/Model.cs +++ b/FModel/Views/Snooper/Models/Model.cs @@ -13,10 +13,11 @@ using CUE4Parse.UE4.Assets.Exports.Material; using CUE4Parse.UE4.Assets.Exports.SkeletalMesh; using CUE4Parse.UE4.Assets.Exports.StaticMesh; using CUE4Parse.UE4.Objects.Core.Math; +using CUE4Parse.UE4.Objects.Core.Misc; using FModel.Extensions; using FModel.Settings; +using FModel.Views.Snooper.Animations; using FModel.Views.Snooper.Buffers; -using FModel.Views.Snooper.Models.Animations; using FModel.Views.Snooper.Shading; using OpenTK.Graphics.OpenGL4; @@ -69,6 +70,7 @@ public class Model : IDisposable private const int _faceSize = 3; private readonly UObject _export; + public readonly FGuid Guid; public readonly string Path; public readonly string Name; public readonly string Type; @@ -107,9 +109,10 @@ public class Model : IDisposable public int SelectedInstance; public float MorphTime; - protected Model(UObject export) + protected Model(UObject export, FGuid guid) { _export = export; + Guid = guid; Path = _export.GetPathName(); Name = Path.SubstringAfterLast('/').SubstringBefore('.'); Type = export.ExportType; @@ -120,8 +123,8 @@ public class Model : IDisposable Transforms = new List(); } - public Model(UStaticMesh export, CStaticMesh staticMesh) : this(export, staticMesh, Transform.Identity) {} - public Model(UStaticMesh export, CStaticMesh staticMesh, Transform transform) : this(export, export.Materials, staticMesh.LODs, transform) + public Model(UStaticMesh export, FGuid guid, CStaticMesh staticMesh) : this(export, guid, staticMesh, Transform.Identity) {} + public Model(UStaticMesh export, FGuid guid, CStaticMesh staticMesh, Transform transform) : this(export, guid, export.Materials, staticMesh.LODs, transform) { Box = staticMesh.BoundingBox * Constants.SCALE_DOWN_RATIO; @@ -132,8 +135,8 @@ public class Model : IDisposable } } - public Model(USkeletalMesh export, CSkeletalMesh skeletalMesh) : this(export, skeletalMesh, Transform.Identity) {} - private Model(USkeletalMesh export, CSkeletalMesh skeletalMesh, Transform transform) : this(export, export.Materials, skeletalMesh.LODs, transform) + public Model(USkeletalMesh export, FGuid guid, CSkeletalMesh skeletalMesh) : this(export, guid, skeletalMesh, Transform.Identity) {} + private Model(USkeletalMesh export, FGuid guid, CSkeletalMesh skeletalMesh, Transform transform) : this(export, guid, export.Materials, skeletalMesh.LODs, transform) { Box = skeletalMesh.BoundingBox * Constants.SCALE_DOWN_RATIO; Skeleton = new Skeleton(export.ReferenceSkeleton); @@ -159,11 +162,11 @@ public class Model : IDisposable } } - private Model(UObject export, IReadOnlyList materials, IReadOnlyList lods, Transform transform = null) - : this(export, materials, lods[_LOD_INDEX], lods[_LOD_INDEX].Verts, lods.Count, transform) {} - private Model(UObject export, IReadOnlyList materials, IReadOnlyList lods, Transform transform = null) - : this(export, materials, lods[_LOD_INDEX], lods[_LOD_INDEX].Verts, lods.Count, transform) {} - private Model(UObject export, IReadOnlyList materials, CBaseMeshLod lod, IReadOnlyList vertices, int numLods, Transform transform = null) : this(export) + private Model(UObject export, FGuid guid, IReadOnlyList materials, IReadOnlyList lods, Transform transform = null) + : this(export, guid, materials, lods[_LOD_INDEX], lods[_LOD_INDEX].Verts, lods.Count, transform) {} + private Model(UObject export, FGuid guid, IReadOnlyList materials, IReadOnlyList lods, Transform transform = null) + : this(export, guid, materials, lods[_LOD_INDEX], lods[_LOD_INDEX].Verts, lods.Count, transform) {} + private Model(UObject export, FGuid guid, IReadOnlyList materials, CBaseMeshLod lod, IReadOnlyList vertices, int numLods, Transform transform = null) : this(export, guid) { var hasCustomUvs = lod.ExtraUV.IsValueCreated; UvCount = hasCustomUvs ? Math.Max(lod.NumTexCoords, numLods) : lod.NumTexCoords; @@ -248,19 +251,14 @@ public class Model : IDisposable Transforms.Add(transform); } - public void UpdateMatrices(Options options, float deltaSeconds = 0f, bool update = false) + public void UpdateMatrices(Options options) { var worldMatrix = UpdateMatrices(); - if (update && HasSkeleton) Skeleton.UpdateMatrices(deltaSeconds); foreach (var socket in Sockets) { var boneMatrix = Matrix4x4.Identity; if (HasSkeleton && Skeleton.BonesIndicesByLoweredName.TryGetValue(socket.BoneName.Text.ToLower(), out var boneIndices)) - { - boneMatrix = Skeleton.HasAnim - ? Skeleton.Anim.InterpolateBoneTransform(boneIndices.BoneIndex) - : Skeleton.BonesTransformByIndex[boneIndices.BoneIndex].Matrix; - } + boneMatrix = Skeleton.GetBoneMatrix(boneIndices); var socketRelation = boneMatrix * worldMatrix; foreach (var attached in socket.AttachedModels) @@ -295,6 +293,8 @@ public class Model : IDisposable public void AttachModel(Model attachedTo, Socket socket) { + socket.AttachedModels.Add(Guid); + _attachedTo = $"'{socket.Name}' from '{attachedTo.Name}'{(!socket.BoneName.IsNone ? $" at '{socket.BoneName}'" : "")}"; attachedTo._attachedFor.Add($"'{Name}'"); // reset PRS to 0 so it's attached to the actual position (can be transformed relative to the socket later by the user) @@ -303,8 +303,10 @@ public class Model : IDisposable Transforms[SelectedInstance].Scale = FVector.OneVector; } - public void DetachModel(Model attachedTo) + public void DetachModel(Model attachedTo, Socket socket) { + socket.AttachedModels.Remove(Guid); + _attachedTo = string.Empty; attachedTo._attachedFor.Remove($"'{Name}'"); Transforms[SelectedInstance].Relation = _previousMatrix; diff --git a/FModel/Views/Snooper/Options.cs b/FModel/Views/Snooper/Options.cs index 52e68474..3eb67cac 100644 --- a/FModel/Views/Snooper/Options.cs +++ b/FModel/Views/Snooper/Options.cs @@ -4,6 +4,7 @@ using CUE4Parse_Conversion.Textures; using CUE4Parse.UE4.Assets.Exports.Texture; using CUE4Parse.UE4.Objects.Core.Misc; using FModel.Settings; +using FModel.Views.Snooper.Animations; using FModel.Views.Snooper.Lights; using FModel.Views.Snooper.Models; using FModel.Views.Snooper.Shading; @@ -15,11 +16,15 @@ public class Options public FGuid SelectedModel { get; private set; } public int SelectedSection { get; private set; } public int SelectedMorph { get; private set; } + public int SelectedAnimation { get; private set; } public readonly Dictionary Models; public readonly Dictionary Textures; public readonly List Lights; + public readonly TimeTracker Tracker; + public readonly List Animations; + public readonly Dictionary Icons; private ETexturePlatform _platform; @@ -30,6 +35,9 @@ public class Options Textures = new Dictionary(); Lights = new List(); + Tracker = new TimeTracker(); + Animations = new List(); + Icons = new Dictionary { ["material"] = new ("materialicon"), @@ -80,6 +88,11 @@ public class Options SelectedMorph = 0; } + public void SelectAnimation() + { + + } + public void SelectSection(int index) { SelectedSection = index; @@ -136,7 +149,7 @@ public class Options Services.ApplicationService.ApplicationView.CUE4Parse.ModelIsWaitingAnimation = value; } - public void ResetModelsAndLights() + public void ResetModelsLightsAnimations() { foreach (var model in Models.Values) { @@ -144,11 +157,17 @@ public class Options } Models.Clear(); Lights.Clear(); + Tracker.Reset(); + foreach (var animation in Animations) + { + animation.Dispose(); + } + Animations.Clear(); } public void Dispose() { - ResetModelsAndLights(); + ResetModelsLightsAnimations(); foreach (var texture in Textures.Values) { texture.Dispose(); diff --git a/FModel/Views/Snooper/Renderer.cs b/FModel/Views/Snooper/Renderer.cs index a2fc1aad..7f6f1584 100644 --- a/FModel/Views/Snooper/Renderer.cs +++ b/FModel/Views/Snooper/Renderer.cs @@ -19,6 +19,7 @@ using CUE4Parse.UE4.Objects.UObject; using FModel.Creator; using FModel.Extensions; using FModel.Settings; +using FModel.Views.Snooper.Animations; using FModel.Views.Snooper.Buffers; using FModel.Views.Snooper.Lights; using FModel.Views.Snooper.Models; @@ -100,19 +101,27 @@ public class Renderer : IDisposable private void Animate(UObject anim, Model model) { if (!model.HasSkeleton) return; + + float maxElapsedTime; switch (anim) { case UAnimSequence animSequence when animSequence.Skeleton.TryLoad(out USkeleton skeleton): - model.Skeleton.SetAnimation(skeleton.ConvertAnims(animSequence), AnimateWithRotationOnly); + { + var animSet = skeleton.ConvertAnims(animSequence); + var animation = new Animation(animSet, model.Guid); + maxElapsedTime = animation.TotalElapsedTime; + model.Skeleton.Animate(animSet, AnimateWithRotationOnly); + Options.Animations.Add(animation); break; + } case UAnimMontage animMontage when animMontage.Skeleton.TryLoad(out USkeleton skeleton): - // for (int i = 0; i < skeleton.Sockets.Length; i++) - // { - // if (skeleton.Sockets[i].Load() is not { } socket) continue; - // model.Sockets.Add(new Socket(socket)); - // } + { + var animSet = skeleton.ConvertAnims(animMontage); + var animation = new Animation(animSet, model.Guid); + maxElapsedTime = animation.TotalElapsedTime; + model.Skeleton.Animate(animSet, AnimateWithRotationOnly); + Options.Animations.Add(animation); - model.Skeleton.SetAnimation(skeleton.ConvertAnims(animMontage), AnimateWithRotationOnly); foreach (var notifyEvent in animMontage.Notifies) { if (!notifyEvent.NotifyStateClass.TryLoad(out UObject notifyClass) || @@ -134,7 +143,7 @@ public class Renderer : IDisposable if (notifyClass.TryGetValue(out FName socketName, "SocketName")) { var t = Transform.Identity; - if (notifyClass.TryGetValue(out FVector location, "Location")) + if (notifyClass.TryGetValue(out FVector location, "LocationOffset", "Location")) t.Position = location * Constants.SCALE_DOWN_RATIO; if (notifyClass.TryGetValue(out FRotator rotation, "RotationOffset", "Rotation")) t.Rotation = rotation.Quaternion(); @@ -143,15 +152,25 @@ public class Renderer : IDisposable var s = new Socket("hello", socketName, t); model.Sockets.Add(s); - s.AttachedModels.Add(guid); addedModel.AttachModel(model, s); } } break; + } case UAnimComposite animComposite when animComposite.Skeleton.TryLoad(out USkeleton skeleton): - model.Skeleton.SetAnimation(skeleton.ConvertAnims(animComposite), AnimateWithRotationOnly); + { + var animSet = skeleton.ConvertAnims(animComposite); + var animation = new Animation(animSet, model.Guid); + maxElapsedTime = animation.TotalElapsedTime; + model.Skeleton.Animate(animSet, AnimateWithRotationOnly); + Options.Animations.Add(animation); break; + } + default: + throw new ArgumentException(); } + + Options.Tracker.SetMaxElapsedTime(maxElapsedTime); Options.AnimateMesh(false); } @@ -180,10 +199,21 @@ public class Renderer : IDisposable for (int i = 0; i < 5; i++) _shader.SetUniform($"bVertexColors[{i}]", i == VertexColor); + // update animations + if (Options.Animations.Count > 0) Options.Tracker.Update(deltaSeconds); + foreach (var animation in Options.Animations) + { + animation.TimeCalculation(Options.Tracker.ElapsedTime); + foreach (var guid in animation.AttachedModels.Where(guid => Options.Models[guid].HasSkeleton)) + { + Options.Models[guid].Skeleton.UpdateAnimationMatrices(animation.CurrentSequence, animation.FrameInSequence); + } + } + // render model pass foreach (var model in Options.Models.Values) { - model.UpdateMatrices(Options, deltaSeconds, model.Show); + model.UpdateMatrices(Options); if (!model.Show) continue; model.Render(_shader); } @@ -225,7 +255,7 @@ public class Renderer : IDisposable if (!original.TryConvert(out var mesh)) return guid; - Options.Models[guid] = new Model(original, mesh); + Options.Models[guid] = new Model(original, guid, mesh); if (select) { Options.SelectModel(guid); @@ -239,7 +269,7 @@ public class Renderer : IDisposable var guid = new FGuid((uint) original.GetFullName().GetHashCode()); if (Options.Models.ContainsKey(guid) || !original.TryConvert(out var mesh)) return guid; - Options.Models[guid] = new Model(original, mesh); + Options.Models[guid] = new Model(original, guid, mesh); if (select) { Options.SelectModel(guid); @@ -264,7 +294,7 @@ public class Renderer : IDisposable if (!editorCube.TryConvert(out var mesh)) return; - Options.Models[guid] = new Cube(mesh, original); + Options.Models[guid] = new Cube(mesh, guid, original); Options.SelectModel(guid); SetupCamera(Options.Models[guid].Box); } @@ -356,7 +386,7 @@ public class Renderer : IDisposable } else if (m.TryConvert(out var mesh)) { - model = new Model(m, mesh, t); + model = new Model(m, guid, mesh, t); model.TwoSided = actor.GetOrDefault("bMirrored", staticMeshComp.GetOrDefault("bDisallowMeshPaintPerInstance", model.TwoSided)); if (actor.TryGetValue(out FPackageIndex baseMaterial, "BaseMaterial") && @@ -456,7 +486,7 @@ public class Renderer : IDisposable public void Save() { - Options.ResetModelsAndLights(); + Options.ResetModelsLightsAnimations(); Options.SelectModel(Guid.Empty); Options.SwapMaterial(false); Options.AnimateMesh(false); diff --git a/FModel/Views/Snooper/SnimGui.cs b/FModel/Views/Snooper/SnimGui.cs index a3b90299..d9d0b31e 100644 --- a/FModel/Views/Snooper/SnimGui.cs +++ b/FModel/Views/Snooper/SnimGui.cs @@ -8,8 +8,8 @@ using OpenTK.Windowing.Common; using System.Numerics; using System.Text; using FModel.Settings; +using FModel.Views.Snooper.Animations; using FModel.Views.Snooper.Models; -using FModel.Views.Snooper.Models.Animations; using FModel.Views.Snooper.Shading; using OpenTK.Graphics.OpenGL4; @@ -71,7 +71,7 @@ public class SnimGui DrawDockSpace(s.Size); SectionWindow("Material Inspector", s.Renderer, DrawMaterialInspector, false); - AnimationWindow("Timeline", s.Renderer, (icons, skeleton) => skeleton.Anim.ImGuiTimeline(Controller.FontSemiBold)); + AnimationWindow("Timeline", s.Renderer, (icons, tracker, animations) => tracker.ImGuiTimeline(Controller.FontSemiBold, animations)); Window("World", () => DrawWorld(s), false); @@ -407,7 +407,6 @@ Snooper aims to give an accurate preview of models, materials, skeletal animatio { MeshWindow("Sockets", s.Renderer, (icons, selectedModel) => { - var selectedGuid = s.Renderer.Options.SelectedModel; foreach (var model in s.Renderer.Options.Models.Values) { if (!model.HasSockets || model.IsSelected) continue; @@ -416,18 +415,16 @@ Snooper aims to give an accurate preview of models, materials, skeletal animatio var i = 0; foreach (var socket in model.Sockets) { - var isAttached = socket.AttachedModels.Contains(selectedGuid); + var isAttached = socket.AttachedModels.Contains(selectedModel.Guid); ImGui.PushID(i); ImGui.BeginDisabled(selectedModel.IsAttached && !isAttached); switch (isAttached) { case false when ImGui.Button($"Attach to '{socket.Name}'"): - socket.AttachedModels.Add(selectedGuid); selectedModel.AttachModel(model, socket); break; case true when ImGui.Button($"Detach from '{socket.Name}'"): - socket.AttachedModels.Remove(selectedGuid); - selectedModel.DetachModel(model); + selectedModel.DetachModel(model, socket); break; } ImGui.EndDisabled(); @@ -768,15 +765,10 @@ Snooper aims to give an accurate preview of models, materials, skeletal animatio }, styled); } - private void AnimationWindow(string name, Renderer renderer, Action, Skeleton> content, bool styled = true) + private void AnimationWindow(string name, Renderer renderer, Action, TimeTracker, List> content, bool styled = true) { ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero); - MeshWindow(name, renderer, (icons, model) => - { - if (!model.HasSkeleton) CenteredTextColored(_errorColor, "No Skeleton To Animate"); - else if (!model.Skeleton.HasAnim) CenteredTextColored(_errorColor, "Mesh Not Animated"); - else content(icons, model.Skeleton); - }, styled); + Window(name, () => content(renderer.Options.Icons, renderer.Options.Tracker, renderer.Options.Animations), styled); ImGui.PopStyleVar(); }