mirror of
https://github.com/4sval/FModel.git
synced 2026-06-21 07:20:05 -05:00
timeline is world relative - part1
This commit is contained in:
parent
8438591839
commit
5b5dd8be53
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
75
FModel/Views/Snooper/Animations/Animation.cs
Normal file
75
FModel/Views/Snooper/Animations/Animation.cs
Normal file
|
|
@ -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<FGuid> AttachedModels;
|
||||
|
||||
public Animation()
|
||||
{
|
||||
Sequences = Array.Empty<Sequence>();
|
||||
AttachedModels = new List<FGuid>();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace FModel.Views.Snooper.Models.Animations;
|
||||
namespace FModel.Views.Snooper.Animations;
|
||||
|
||||
public class BoneIndice
|
||||
{
|
||||
40
FModel/Views/Snooper/Animations/Sequence.cs
Normal file
40
FModel/Views/Snooper/Animations/Sequence.cs
Normal file
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
270
FModel/Views/Snooper/Animations/Skeleton.cs
Normal file
270
FModel/Views/Snooper/Animations/Skeleton.cs
Normal file
|
|
@ -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<Matrix4x4> _ssbo;
|
||||
|
||||
public string Name;
|
||||
public readonly Dictionary<string, BoneIndice> BonesIndicesByLoweredName;
|
||||
public readonly Dictionary<int, Transform> 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<string, BoneIndice>();
|
||||
BonesTransformByIndex = new Dictionary<int, Transform>();
|
||||
_animatedBonesTransform = Array.Empty<Transform[][]>();
|
||||
_invertedBonesMatrix = Array.Empty<Matrix4x4>();
|
||||
}
|
||||
|
||||
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<Matrix4x4>(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);
|
||||
}
|
||||
}
|
||||
134
FModel/Views/Snooper/Animations/TimeTracker.cs
Normal file
134
FModel/Views/Snooper/Animations/TimeTracker.cs
Normal file
|
|
@ -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<Animation> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Sequence>();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Matrix4x4> _ssbo;
|
||||
|
||||
public string Name;
|
||||
public readonly Dictionary<string, BoneIndice> BonesIndicesByLoweredName;
|
||||
public readonly Dictionary<int, Transform> BonesTransformByIndex;
|
||||
public readonly Matrix4x4[] InvertedBonesMatrix;
|
||||
public int BoneCount => InvertedBonesMatrix.Length;
|
||||
|
||||
public Animation Anim;
|
||||
public bool HasAnim => Anim != null;
|
||||
|
||||
public Skeleton()
|
||||
{
|
||||
BonesIndicesByLoweredName = new Dictionary<string, BoneIndice>();
|
||||
BonesTransformByIndex = new Dictionary<int, Transform>();
|
||||
InvertedBonesMatrix = Array.Empty<Matrix4x4>();
|
||||
}
|
||||
|
||||
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<Matrix4x4>(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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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];
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Transform>();
|
||||
}
|
||||
|
||||
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<ResolvedObject> materials, IReadOnlyList<CStaticMeshLod> lods, Transform transform = null)
|
||||
: this(export, materials, lods[_LOD_INDEX], lods[_LOD_INDEX].Verts, lods.Count, transform) {}
|
||||
private Model(UObject export, IReadOnlyList<ResolvedObject> materials, IReadOnlyList<CSkelMeshLod> lods, Transform transform = null)
|
||||
: this(export, materials, lods[_LOD_INDEX], lods[_LOD_INDEX].Verts, lods.Count, transform) {}
|
||||
private Model(UObject export, IReadOnlyList<ResolvedObject> materials, CBaseMeshLod lod, IReadOnlyList<CMeshVertex> vertices, int numLods, Transform transform = null) : this(export)
|
||||
private Model(UObject export, FGuid guid, IReadOnlyList<ResolvedObject> materials, IReadOnlyList<CStaticMeshLod> 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<ResolvedObject> materials, IReadOnlyList<CSkelMeshLod> 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<ResolvedObject> materials, CBaseMeshLod lod, IReadOnlyList<CMeshVertex> 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;
|
||||
|
|
|
|||
|
|
@ -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<FGuid, Model> Models;
|
||||
public readonly Dictionary<FGuid, Texture> Textures;
|
||||
public readonly List<Light> Lights;
|
||||
|
||||
public readonly TimeTracker Tracker;
|
||||
public readonly List<Animation> Animations;
|
||||
|
||||
public readonly Dictionary<string, Texture> Icons;
|
||||
|
||||
private ETexturePlatform _platform;
|
||||
|
|
@ -30,6 +35,9 @@ public class Options
|
|||
Textures = new Dictionary<FGuid, Texture>();
|
||||
Lights = new List<Light>();
|
||||
|
||||
Tracker = new TimeTracker();
|
||||
Animations = new List<Animation>();
|
||||
|
||||
Icons = new Dictionary<string, Texture>
|
||||
{
|
||||
["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();
|
||||
|
|
|
|||
|
|
@ -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<USkeletalMeshSocket>() 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);
|
||||
|
|
|
|||
|
|
@ -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<Dictionary<string, Texture>, Skeleton> content, bool styled = true)
|
||||
private void AnimationWindow(string name, Renderer renderer, Action<Dictionary<string, Texture>, TimeTracker, List<Animation>> 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();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user