new timeline part 1

This commit is contained in:
4sval 2023-02-17 04:58:29 +01:00
parent e49216a844
commit 4eb220168e
16 changed files with 208 additions and 60 deletions

View File

@ -230,6 +230,12 @@
<Resource Include="Resources\link_on.png" />
<Resource Include="Resources\link_off.png" />
<Resource Include="Resources\link_has.png" />
<Resource Include="Resources\tl_play.png" />
<Resource Include="Resources\tl_pause.png" />
<Resource Include="Resources\tl_rewind.png" />
<Resource Include="Resources\tl_forward.png" />
<Resource Include="Resources\tl_previous.png" />
<Resource Include="Resources\tl_next.png" />
</ItemGroup>
<ItemGroup>

View File

@ -45,6 +45,7 @@ public partial class MainWindow
private async void OnLoaded(object sender, RoutedEventArgs e)
{
var newOrUpdated = UserSettings.Default.ShowChangelog;
#if !DEBUG
ApplicationService.ApiEndpointView.FModelApi.CheckForUpdates(UserSettings.Default.UpdateMode);
#endif
@ -67,7 +68,7 @@ public partial class MainWindow
await _applicationView.CUE4Parse.InitInformation();
#endif
await _applicationView.CUE4Parse.InitMappings();
await _applicationView.InitImGuiSettings();
await _applicationView.InitImGuiSettings(newOrUpdated);
await _applicationView.InitVgmStream();
await _applicationView.InitOodle();

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 B

View File

@ -1,14 +1,18 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using CUE4Parse_Conversion.Animations;
using CUE4Parse.UE4.Objects.Core.Misc;
using CUE4Parse.Utils;
using ImGuiNET;
namespace FModel.Views.Snooper.Animations;
public class Animation : IDisposable
{
public readonly string Name;
public readonly Sequence[] Sequences;
public readonly float StartTime; // Animation Start Time
public readonly float EndTime; // Animation End Time
public readonly float TotalElapsedTime; // Animation Max Time
@ -23,8 +27,9 @@ public class Animation : IDisposable
AttachedModels = new List<FGuid>();
}
public Animation(CAnimSet animSet) : this()
public Animation(string name, CAnimSet animSet) : this()
{
Name = name;
Sequences = new Sequence[animSet.Sequences.Count];
for (int i = 0; i < Sequences.Length; i++)
{
@ -34,11 +39,11 @@ public class Animation : IDisposable
EndTime = Sequences[i].EndTime;
}
// if (Sequences.Length > 0)
// Tracker.ElapsedTime = Sequences[0].StartTime;
if (Sequences.Length > 0)
StartTime = Sequences[0].StartTime;
}
public Animation(CAnimSet animSet, params FGuid[] animatedModels) : this(animSet)
public Animation(string name, CAnimSet animSet, params FGuid[] animatedModels) : this(name, animSet)
{
AttachedModels.AddRange(animatedModels);
}
@ -72,4 +77,16 @@ public class Animation : IDisposable
{
AttachedModels.Clear();
}
public void ImGuiAnimation(ImDrawListPtr drawList, Vector2 timelineP0, Vector2 treeP0, Vector2 treeP1, Vector2 timeStep, Vector2 timeRatio, float y, float t)
{
var p1 = new Vector2(timelineP0.X + StartTime * timeRatio.X + t, y + t);
var p2 = new Vector2(timelineP0.X + EndTime * timeRatio.X - t, y + timeStep.Y - t);
drawList.AddRectFilled(p1, p2, 0xFF175F17, 5.0f, ImDrawFlags.RoundCornersTop);
drawList.PushClipRect(treeP0, treeP1);
drawList.AddText(treeP0 with { Y = y + timeStep.Y / 4.0f }, 0xFFFFFFFF, Name);
drawList.PopClipRect();
}
}

View File

@ -177,12 +177,7 @@ public class Skeleton : IDisposable
private void TrackSkeleton(CAnimSet anim)
{
// reset
foreach (var boneIndices in BonesIndicesByLoweredName.Values)
{
boneIndices.TrackIndex = -1;
boneIndices.ParentTrackIndex = -1;
}
ResetAnimatedData();
// tracked bones
for (int trackIndex = 0; trackIndex < anim.TrackBonesInfo.Length; trackIndex++)
@ -211,7 +206,7 @@ public class Skeleton : IDisposable
continue;
#if DEBUG
Log.Warning($"Bone Mismatch: {boneName} ({boneIndices.BoneIndex}) was not present in the anim's target skeleton");
Log.Warning($"{Name} Bone Mismatch: {boneName} ({boneIndices.BoneIndex}) was not present in the anim's target skeleton");
#endif
var loweredParentBoneName = boneIndices.LoweredParentBoneName;
@ -224,13 +219,25 @@ public class Skeleton : IDisposable
}
}
public void ResetAnimatedData(bool full = false)
{
foreach (var boneIndices in BonesIndicesByLoweredName.Values)
{
boneIndices.TrackIndex = -1;
boneIndices.ParentTrackIndex = -1;
}
if (!full) return;
_animatedBonesTransform = Array.Empty<Transform[][]>();
_ssbo.UpdateRange(BoneCount, Matrix4x4.Identity);
}
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);
_ssbo.UpdateRange(BoneCount, Matrix4x4.Identity);
}
public void UpdateAnimationMatrices(int currentSequence, int frameInSequence)

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using FModel.Views.Snooper.Shading;
using ImGuiNET;
namespace FModel.Views.Snooper.Animations;
@ -16,34 +17,37 @@ public enum ETrackerType
public class TimeTracker : IDisposable
{
public bool IsPaused;
public bool IsActive;
public float ElapsedTime;
public float MaxElapsedTime { get; private set; }
private float _timeHeight = 10.0f;
private float _timeBarHeight => _timeHeight * 2.0f;
public float MaxElapsedTime;
public TimeTracker()
{
Reset();
SetMaxElapsedTime(0.01f);
}
public void Update(float deltaSeconds)
{
if (IsPaused) return;
if (IsPaused || IsActive) return;
ElapsedTime += deltaSeconds;
if (ElapsedTime >= MaxElapsedTime) Reset();
if (ElapsedTime >= MaxElapsedTime) Reset(false);
}
public void SetMaxElapsedTime(float maxElapsedTime)
public void SafeSetElapsedTime(float elapsedTime)
{
ElapsedTime = Math.Clamp(elapsedTime, 0.0f, MaxElapsedTime);
}
public void SafeSetMaxElapsedTime(float maxElapsedTime)
{
MaxElapsedTime = MathF.Max(maxElapsedTime, MaxElapsedTime);
}
public void Reset()
public void Reset(bool doMet = true)
{
IsPaused = false;
ElapsedTime = 0.0f;
if (doMet) MaxElapsedTime = 0.01f;
}
public void Dispose()
@ -51,52 +55,100 @@ public class TimeTracker : IDisposable
Reset();
}
public void ImGuiTimeline(ImFontPtr fontPtr, List<Animation> animations)
private const float _margin = 5.0f;
private const float _thickness = 2.0f;
private const float _timeHeight = 10.0f;
private float _timeBarHeight => _timeHeight * 2.0f;
private readonly Vector2 _buttonSize = new (14.0f);
private readonly string[] _icons = { "tl_forward", "tl_pause", "tl_rewind" };
public void ImGuiTimeline(Dictionary<string, Texture> icons, List<Animation> animations, Vector2 outliner, int activeAnimation, ImFontPtr fontPtr)
{
var io = ImGui.GetIO();
var canvasP0 = ImGui.GetCursorScreenPos();
var treeP0 = ImGui.GetCursorScreenPos();
var canvasSize = ImGui.GetContentRegionAvail();
var canvasP1 = new Vector2(canvasP0.X + canvasSize.X, canvasP0.Y + canvasSize.Y);
var timeRatio = canvasSize / MaxElapsedTime;
var timelineP1 = new Vector2(treeP0.X + canvasSize.X, treeP0.Y + canvasSize.Y);
var treeP1 = timelineP1 with { X = treeP0.X + outliner.X };
var timelineP0 = treeP0 with { X = treeP1.X + _thickness };
var timelineSize = timelineP1 - timelineP0;
var timeRatio = timelineSize / MaxElapsedTime;
var timeStep = timeRatio * MaxElapsedTime / timelineSize * 50.0f;
timeStep.Y /= 2.0f;
var drawList = ImGui.GetWindowDrawList();
drawList.AddRectFilled(treeP0, treeP1, 0xFF1F1C1C);
drawList.AddRectFilled(timelineP0, timelineP1 with { Y = timelineP0.Y + _timeBarHeight }, 0xFF141414);
drawList.AddRectFilled(timelineP0 with { Y = timelineP0.Y + _timeBarHeight }, timelineP1, 0xFF242424);
drawList.AddLine(new Vector2(treeP1.X, treeP0.Y), treeP1, 0xFF504545, _thickness);
drawList.AddLine(treeP0 with { Y = timelineP0.Y + _timeBarHeight }, timelineP1 with { Y = timelineP0.Y + _timeBarHeight }, 0x50504545, _thickness);
ImGui.InvisibleButton("timeline_canvas", canvasP1 with { Y = _timeBarHeight }, ImGuiButtonFlags.MouseButtonLeft);
IsPaused = ImGui.IsItemActive();
if (IsPaused && ImGui.IsMouseDragging(ImGuiMouseButton.Left))
// adding margin
treeP0.X += _margin;
treeP1.X -= _margin;
// control buttons
for (int i = 0; i < _icons.Length; i++)
{
var mousePosCanvas = io.MousePos - canvasP0;
ElapsedTime = Math.Clamp(mousePosCanvas.X / canvasSize.X * MaxElapsedTime, 0.01f, MaxElapsedTime);
var x = _buttonSize.X * 2.0f * i;
ImGui.SetCursorScreenPos(treeP0 with { X = treeP1.X - x - _buttonSize.X * 2.0f + _thickness });
if (ImGui.ImageButton($"timeline_actions_{_icons[i]}", icons[_icons[i]].GetPointer(), _buttonSize))
{
switch (i)
{
case 0:
SafeSetElapsedTime(ElapsedTime + timeStep.X / timeRatio.X);
break;
case 1:
IsPaused = !IsPaused;
_icons[1] = IsPaused ? "tl_play" : "tl_pause";
break;
case 2:
SafeSetElapsedTime(ElapsedTime - timeStep.X / timeRatio.X);
break;
}
}
}
ImGui.SetCursorScreenPos(timelineP0);
ImGui.InvisibleButton("timeline_timetracker_canvas", timelineSize with { Y = _timeBarHeight }, ImGuiButtonFlags.MouseButtonLeft);
IsActive = ImGui.IsItemActive();
if (IsActive && ImGui.IsMouseDragging(ImGuiMouseButton.Left))
{
var mousePosCanvas = ImGui.GetIO().MousePos - timelineP0;
SafeSetElapsedTime(mousePosCanvas.X / timelineSize.X * 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)
{ // draw time + time grid
for (float x = 0; x < timelineSize.X; x += timeStep.X)
{
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");
var cursor = timelineP0.X + x;
drawList.AddLine(new Vector2(cursor, timelineP0.Y + _timeHeight + 2.5f), new Vector2(cursor, timelineP0.Y + _timeBarHeight), 0xA0FFFFFF);
drawList.AddLine(new Vector2(cursor, timelineP0.Y + _timeBarHeight), timelineP1 with { X = cursor }, 0x28C8C8C8);
drawList.AddText(fontPtr, 14, new Vector2(cursor + 4, timelineP0.Y + 7.5f), 0x50FFFFFF, $"{x / timeRatio.X:F1}s");
}
for (float y = _timeBarHeight; y < timelineSize.Y; y += timeStep.Y)
{
drawList.AddLine(timelineP0 with { Y = timelineP0.Y + y }, timelineP1 with { Y = timelineP0.Y + y }, 0x28C8C8C8);
}
}
drawList.PopClipRect();
foreach (var animation in animations)
for (int i = 0; i < animations.Count; i++)
{
for (int i = 0; i < animation.Sequences.Length; i++)
{
animation.Sequences[i].DrawSequence(drawList, canvasP0.X, canvasP0.Y + _timeBarHeight, timeRatio, i, i == animation.CurrentSequence ? 0xFF0000FF : 0xFF175F17);
}
DrawSeparator(drawList, canvasP0, canvasP1, animation.EndTime * timeRatio.X, ETrackerType.End);
var y = timelineP0.Y + _timeBarHeight + timeStep.Y * (i % 2);
animations[i].ImGuiAnimation(drawList, timelineP0, treeP0, treeP1, timeStep, timeRatio, y, _thickness);
DrawSeparator(drawList, timelineP0, y + timeStep.Y, animations[i].EndTime * timeRatio.X, ETrackerType.End);
}
DrawSeparator(drawList, canvasP0, canvasP1, ElapsedTime * timeRatio.X, ETrackerType.Frame);
DrawSeparator(drawList, timelineP0, timelineP1.Y, ElapsedTime * timeRatio.X, ETrackerType.Frame);
}
private void DrawSeparator(ImDrawListPtr drawList, Vector2 origin, Vector2 destination, float time, ETrackerType separatorType)
private void DrawSeparator(ImDrawListPtr drawList, Vector2 origin, float y, float time, ETrackerType separatorType)
{
const int size = 5;
@ -106,7 +158,7 @@ public class TimeTracker : IDisposable
ETrackerType.End => origin with { X = origin.X + time },
_ => throw new ArgumentOutOfRangeException(nameof(separatorType), separatorType, null)
};
var p2 = new Vector2(p1.X, destination.Y);
var p2 = p1 with { Y = y };
uint color = separatorType switch
{

View File

@ -26,6 +26,13 @@ public class BufferObject<TDataType> : IDisposable where TDataType : unmanaged
GL.BufferData(bufferTarget, length * sizeof(TDataType), IntPtr.Zero, BufferUsageHint.DynamicDraw);
}
public void UpdateRange(int count, TDataType data)
{
Bind();
for (int i = 0; i < count; i++) Update(i, data);
Unbind();
}
public unsafe void Update(int offset, TDataType data)
{
GL.BufferSubData(_bufferTarget, (IntPtr) (offset * sizeof(TDataType)), sizeof(TDataType), ref data);

View File

@ -81,6 +81,7 @@ public class Model : IDisposable
public Section[] Sections;
public Material[] Materials;
public bool TwoSided;
public bool IsAnimatedProp;
public bool HasSkeleton => Skeleton != null;
public readonly Skeleton Skeleton;
@ -306,7 +307,11 @@ public class Model : IDisposable
public void DetachModel(Model attachedTo, Socket socket)
{
socket.AttachedModels.Remove(Guid);
SafeDetachModel(attachedTo);
}
public void SafeDetachModel(Model attachedTo)
{
_attachedTo = string.Empty;
attachedTo._attachedFor.Remove($"'{Name}'");
Transforms[SelectedInstance].Relation = _previousMatrix;

View File

@ -47,6 +47,12 @@ public class Options
["link_on"] = new ("link_on"),
["link_off"] = new ("link_off"),
["link_has"] = new ("link_has"),
["tl_play"] = new ("tl_play"),
["tl_pause"] = new ("tl_pause"),
["tl_rewind"] = new ("tl_rewind"),
["tl_forward"] = new ("tl_forward"),
["tl_previous"] = new ("tl_previous"),
["tl_next"] = new ("tl_next"),
};
_platform = UserSettings.Default.OverridedPlatform;
@ -88,9 +94,50 @@ public class Options
SelectedMorph = 0;
}
public void SelectAnimation()
public void RemoveModel(FGuid guid)
{
if (!TryGetModel(guid, out var model)) return;
DetachAndRemoveModels(model);
model.Dispose();
Models.Remove(guid);
}
private void DetachAndRemoveModels(Model model)
{
foreach (var socket in model.Sockets)
{
foreach (var guid in socket.AttachedModels)
{
if (!TryGetModel(guid, out var attachedModel)) continue;
attachedModel.SafeDetachModel(model);
RemoveModel(guid);
}
socket.Dispose();
}
}
public void AddAnimation(Animation animation)
{
Animations.Add(animation);
}
public void RemoveAnimations()
{
Tracker.Reset();
foreach (var animation in Animations)
{
foreach (var guid in animation.AttachedModels)
{
if (!TryGetModel(guid, out var animatedModel)) continue;
animatedModel.Skeleton.ResetAnimatedData(true);
DetachAndRemoveModels(animatedModel);
}
animation.Dispose();
}
Animations.Clear();
}
public void SelectSection(int index)

View File

@ -108,19 +108,19 @@ public class Renderer : IDisposable
case UAnimSequence animSequence when animSequence.Skeleton.TryLoad(out USkeleton skeleton):
{
var animSet = skeleton.ConvertAnims(animSequence);
var animation = new Animation(animSet, model.Guid);
var animation = new Animation(animSequence.Name, animSet, model.Guid);
maxElapsedTime = animation.TotalElapsedTime;
model.Skeleton.Animate(animSet, AnimateWithRotationOnly);
Options.Animations.Add(animation);
Options.AddAnimation(animation);
break;
}
case UAnimMontage animMontage when animMontage.Skeleton.TryLoad(out USkeleton skeleton):
{
var animSet = skeleton.ConvertAnims(animMontage);
var animation = new Animation(animSet, model.Guid);
var animation = new Animation(animMontage.Name, animSet, model.Guid);
maxElapsedTime = animation.TotalElapsedTime;
model.Skeleton.Animate(animSet, AnimateWithRotationOnly);
Options.Animations.Add(animation);
Options.AddAnimation(animation);
foreach (var notifyEvent in animMontage.Notifies)
{
@ -138,6 +138,7 @@ public class Renderer : IDisposable
if (!Options.TryGetModel(guid, out var addedModel))
continue;
addedModel.IsAnimatedProp = true;
if (notifyClass.TryGetValue(out UObject skeletalMeshPropAnimation, "SkeletalMeshPropAnimation"))
Animate(skeletalMeshPropAnimation, addedModel);
if (notifyClass.TryGetValue(out FName socketName, "SocketName"))
@ -150,7 +151,7 @@ public class Renderer : IDisposable
if (notifyClass.TryGetValue(out FVector scale, "Scale"))
t.Scale = scale;
var s = new Socket("hello", socketName, t);
var s = new Socket($"TL_{addedModel.Name}", socketName, t);
model.Sockets.Add(s);
addedModel.AttachModel(model, s);
}
@ -160,17 +161,17 @@ public class Renderer : IDisposable
case UAnimComposite animComposite when animComposite.Skeleton.TryLoad(out USkeleton skeleton):
{
var animSet = skeleton.ConvertAnims(animComposite);
var animation = new Animation(animSet, model.Guid);
var animation = new Animation(animComposite.Name, animSet, model.Guid);
maxElapsedTime = animation.TotalElapsedTime;
model.Skeleton.Animate(animSet, AnimateWithRotationOnly);
Options.Animations.Add(animation);
Options.AddAnimation(animation);
break;
}
default:
throw new ArgumentException();
}
Options.Tracker.SetMaxElapsedTime(maxElapsedTime);
Options.Tracker.SafeSetMaxElapsedTime(maxElapsedTime);
Options.AnimateMesh(false);
}

View File

@ -47,6 +47,8 @@ public class SnimGui
private readonly Save _saver = new ();
private readonly string _renderer;
private readonly string _version;
private Vector2 _outlinerSize;
private bool _ti_open;
private bool _viewportFocus;
@ -71,7 +73,8 @@ public class SnimGui
DrawDockSpace(s.Size);
SectionWindow("Material Inspector", s.Renderer, DrawMaterialInspector, false);
AnimationWindow("Timeline", s.Renderer, (icons, tracker, animations) => tracker.ImGuiTimeline(Controller.FontSemiBold, animations));
AnimationWindow("Timeline", s.Renderer, (icons, tracker, animations) =>
tracker.ImGuiTimeline(icons, animations, _outlinerSize, s.Renderer.Options.SelectedAnimation, Controller.FontSemiBold));
Window("World", () => DrawWorld(s), false);
@ -305,6 +308,7 @@ Snooper aims to give an accurate preview of models, materials, skeletal animatio
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero);
Window("Outliner", () =>
{
_outlinerSize = ImGui.GetWindowSize();
if (ImGui.BeginTable("Items", 4, ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersOuterV | ImGuiTableFlags.NoSavedSettings, ImGui.GetContentRegionAvail()))
{
ImGui.TableSetupColumn("Instance", ImGuiTableColumnFlags.NoHeaderWidth | ImGuiTableColumnFlags.WidthFixed, _tableWidth);
@ -349,6 +353,7 @@ Snooper aims to give an accurate preview of models, materials, skeletal animatio
ImGui.BeginDisabled(!model.HasSkeleton);
if (ImGui.Selectable("Animate"))
{
s.Renderer.Options.RemoveAnimations();
s.Renderer.Options.AnimateMesh(true);
s.WindowShouldClose(true, false);
}
@ -359,7 +364,7 @@ Snooper aims to give an accurate preview of models, materials, skeletal animatio
s.Renderer.CameraOp.Teleport(instancePos, model.Box);
}
if (ImGui.Selectable("Delete")) s.Renderer.Options.Models.Remove(guid);
if (ImGui.Selectable("Delete")) s.Renderer.Options.RemoveModel(guid);
if (ImGui.Selectable("Deselect")) s.Renderer.Options.SelectModel(Guid.Empty);
ImGui.Separator();
if (ImGui.Selectable("Copy Name to Clipboard")) ImGui.SetClipboardText(model.Name);