timeline is world relative - part1

This commit is contained in:
4sval 2023-02-16 19:47:19 +01:00
parent 8438591839
commit 5b5dd8be53
14 changed files with 618 additions and 550 deletions

View File

@ -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
}

View 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();
}
}

View File

@ -1,4 +1,4 @@
namespace FModel.Views.Snooper.Models.Animations;
namespace FModel.Views.Snooper.Animations;
public class BoneIndice
{

View 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();
}
}

View 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);
}
}

View 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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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];

View File

@ -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;

View File

@ -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();

View File

@ -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);

View File

@ -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();
}