mirror of
https://github.com/4sval/FModel.git
synced 2026-08-25 02:44:29 -05:00
only parse bone tracks + anim montage test
This commit is contained in:
Submodule CUE4Parse updated: 9b7bb06564...e69c4c17ca
@@ -75,15 +75,15 @@ public partial class MainWindow
|
||||
_discordHandler.Initialize(_applicationView.CUE4Parse.Game);
|
||||
|
||||
#if DEBUG
|
||||
await _threadWorkerView.Begin(cancellationToken =>
|
||||
_applicationView.CUE4Parse.Extract(cancellationToken,
|
||||
"MoonMan/Content/DeliverUsTheMoon/Characters/Astronaut/SK_Astronaut.uasset"));
|
||||
await _threadWorkerView.Begin(cancellationToken =>
|
||||
_applicationView.CUE4Parse.Extract(cancellationToken,
|
||||
"MoonMan/Content/DeliverUsTheMoon/Characters/Astronaut/AM_ControllingASE.uasset"));
|
||||
// await _threadWorkerView.Begin(cancellationToken =>
|
||||
// _applicationView.CUE4Parse.Extract(cancellationToken,
|
||||
// "FortniteGame/Content/Characters/Player/Female/Medium/Bodies/F_MED_RoseDust/Meshes/F_MED_RoseDust.uasset"));
|
||||
// await _threadWorkerView.Begin(cancellationToken =>
|
||||
// _applicationView.CUE4Parse.Extract(cancellationToken,
|
||||
// "fortnitegame/Content/Accessories/FORT_Backpacks/Backpack_M_MED_Despair/Meshes/M_MED_Despair_Pack.uasset"));
|
||||
// await _threadWorkerView.Begin(cancellationToken =>
|
||||
// _applicationView.CUE4Parse.Extract(cancellationToken,
|
||||
// "FortniteGame/Content/Animation/Game/MainPlayer/Emotes/Acrobatic_Superhero/Emote_AcrobaticSuperhero_CMM.uasset"));
|
||||
// "Hk_project/Content/Animation/CAT/Jump/CAT_JUMP_L050_H-050.uasset"));
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -816,9 +816,10 @@ public class CUE4ParseViewModel : ViewModel
|
||||
SnooperViewer.Run();
|
||||
return true;
|
||||
}
|
||||
case UAnimSequence a when isNone && ModelIsWaitingAnimation:
|
||||
case UAnimSequence when isNone && ModelIsWaitingAnimation:
|
||||
case UAnimMontage when isNone && ModelIsWaitingAnimation:
|
||||
{
|
||||
SnooperViewer.Renderer.Animate(a);
|
||||
SnooperViewer.Renderer.Animate(export);
|
||||
SnooperViewer.Run();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using CUE4Parse_Conversion.Animations;
|
||||
using CUE4Parse.Utils;
|
||||
@@ -9,96 +8,50 @@ namespace FModel.Views.Snooper.Models.Animations;
|
||||
|
||||
public class Animation : IDisposable
|
||||
{
|
||||
public int Frame;
|
||||
public float ElapsedTime;
|
||||
public readonly int MaxFrame;
|
||||
public readonly float FramesPerSecond;
|
||||
public readonly Dictionary<int, int> TrackIndexByBoneIndex;
|
||||
public readonly Transform[][] BoneTransforms;
|
||||
|
||||
private float TimePerFrame => 1.0f / FramesPerSecond;
|
||||
public int CurrentSequence;
|
||||
public readonly Sequence[] Sequences;
|
||||
public int SequencesCount => Sequences.Length;
|
||||
|
||||
public Animation(Skeleton skeleton, CAnimSet anim, bool rotationOnly)
|
||||
public Animation()
|
||||
{
|
||||
Frame = 0;
|
||||
ElapsedTime = 0;
|
||||
TrackIndexByBoneIndex = new Dictionary<int, int>();
|
||||
Sequences = Array.Empty<Sequence>();
|
||||
}
|
||||
|
||||
var sequence = anim.Sequences[0];
|
||||
MaxFrame = sequence.NumFrames;
|
||||
FramesPerSecond = sequence.Rate;
|
||||
|
||||
BoneTransforms = new Transform[skeleton.UnrealSkeleton.ReferenceSkeleton.FinalRefBoneInfo.Length][];
|
||||
for (var trackIndex = 0; trackIndex < BoneTransforms.Length; trackIndex++)
|
||||
public Animation(Skeleton skeleton, CAnimSet anim, bool rotationOnly) : this()
|
||||
{
|
||||
Sequences = new Sequence[anim.Sequences.Count];
|
||||
for (int i = 0; i < Sequences.Length; i++)
|
||||
{
|
||||
BoneTransforms[trackIndex] = new Transform[MaxFrame];
|
||||
|
||||
var bone = skeleton.UnrealSkeleton.ReferenceSkeleton.FinalRefBoneInfo[trackIndex];
|
||||
if (!skeleton.BonesIndexByLoweredName.TryGetValue(bone.Name.Text.ToLower(), out var boneIndex))
|
||||
continue;
|
||||
if (!skeleton.BonesTransformByIndex.TryGetValue(boneIndex, out var originalTransform))
|
||||
throw new ArgumentNullException($"no transform for bone '{boneIndex}'");
|
||||
|
||||
TrackIndexByBoneIndex[boneIndex] = trackIndex;
|
||||
|
||||
for (var frame = 0; frame < BoneTransforms[trackIndex].Length; frame++)
|
||||
{
|
||||
var boneOrientation = originalTransform.Rotation;
|
||||
var bonePosition = originalTransform.Position;
|
||||
var boneScale = originalTransform.Scale;
|
||||
|
||||
sequence.Tracks[trackIndex].GetBonePosition(frame, MaxFrame, false, ref bonePosition, ref boneOrientation);
|
||||
if (frame < sequence.Tracks[trackIndex].KeyScale.Length)
|
||||
boneScale = sequence.Tracks[trackIndex].KeyScale[frame];
|
||||
|
||||
// revert FixRotationKeys
|
||||
if (trackIndex > 0) boneOrientation.Conjugate();
|
||||
|
||||
bonePosition *= Constants.SCALE_DOWN_RATIO;
|
||||
|
||||
switch (anim.BoneModes[trackIndex])
|
||||
{
|
||||
case EBoneRetargetingMode.Animation:
|
||||
case EBoneRetargetingMode.Mesh:
|
||||
case EBoneRetargetingMode.AnimationScaled:
|
||||
case EBoneRetargetingMode.AnimationRelative:
|
||||
case EBoneRetargetingMode.OrientAndScale:
|
||||
case EBoneRetargetingMode.Count:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
BoneTransforms[trackIndex][frame] = new Transform
|
||||
{
|
||||
Relation = bone.ParentIndex >= 0 ? BoneTransforms[bone.ParentIndex][frame].Matrix : originalTransform.Relation,
|
||||
Rotation = boneOrientation,
|
||||
Position = rotationOnly ? originalTransform.Position : bonePosition,
|
||||
Scale = boneScale
|
||||
};
|
||||
}
|
||||
Sequences[i] = new Sequence(anim.Sequences[i], skeleton, rotationOnly);
|
||||
}
|
||||
}
|
||||
|
||||
public void Update(float deltaSeconds)
|
||||
{
|
||||
ElapsedTime += deltaSeconds / TimePerFrame;
|
||||
Frame = ElapsedTime.FloorToInt() % MaxFrame;
|
||||
ElapsedTime += deltaSeconds / Sequences[CurrentSequence].TimePerFrame;
|
||||
Sequences[CurrentSequence].Frame = ElapsedTime.FloorToInt() % Sequences[CurrentSequence].MaxFrame;
|
||||
}
|
||||
|
||||
public Matrix4x4 InterpolateBoneTransform(int trackIndex)
|
||||
{
|
||||
// interpolate here
|
||||
return BoneTransforms[trackIndex][Frame].Matrix;
|
||||
return Sequences[CurrentSequence].BonesTransform[trackIndex][Sequences[CurrentSequence].Frame].Matrix;
|
||||
}
|
||||
|
||||
public void ImGuiTimeline()
|
||||
{
|
||||
ImGui.Text($"Frame: {Frame}/{MaxFrame}");
|
||||
ImGui.Text($"FPS: {FramesPerSecond}");
|
||||
ImGui.BeginDisabled(SequencesCount < 2);
|
||||
ImGui.DragInt("Sequence", ref CurrentSequence, 1, 0, Sequences.Length - 1, Sequences[CurrentSequence].Name);
|
||||
ImGui.EndDisabled();
|
||||
ImGui.Text($"Frame: {Sequences[CurrentSequence].Frame}/{Sequences[CurrentSequence].MaxFrame}");
|
||||
ImGui.Text($"FPS: {Sequences[CurrentSequence].FramesPerSecond}");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
TrackIndexByBoneIndex.Clear();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
76
FModel/Views/Snooper/Models/Animations/Sequence.cs
Normal file
76
FModel/Views/Snooper/Models/Animations/Sequence.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using CUE4Parse_Conversion.Animations;
|
||||
|
||||
namespace FModel.Views.Snooper.Models.Animations;
|
||||
|
||||
public class Sequence : IDisposable
|
||||
{
|
||||
public int Frame;
|
||||
public readonly string Name;
|
||||
public readonly int MaxFrame;
|
||||
public readonly float FramesPerSecond;
|
||||
|
||||
public float TimePerFrame => 1.0f / FramesPerSecond;
|
||||
|
||||
public readonly Transform[][] BonesTransform;
|
||||
|
||||
public Sequence(CAnimSequence sequence, Skeleton skeleton, bool rotationOnly)
|
||||
{
|
||||
Frame = 0;
|
||||
Name = sequence.Name;
|
||||
MaxFrame = sequence.NumFrames;
|
||||
FramesPerSecond = sequence.Rate;
|
||||
|
||||
BonesTransform = new Transform[skeleton.BonesTransformByIndex.Count][];
|
||||
for (int trackIndex = 0; trackIndex < skeleton.UnrealSkeleton.ReferenceSkeleton.FinalRefBoneInfo.Length; trackIndex++)
|
||||
{
|
||||
var bone = skeleton.UnrealSkeleton.ReferenceSkeleton.FinalRefBoneInfo[trackIndex];
|
||||
if (!skeleton.BonesIndicesByLoweredName.TryGetValue(bone.Name.Text.ToLower(), out var boneIndices))
|
||||
continue;
|
||||
|
||||
var originalTransform = skeleton.BonesTransformByIndex[boneIndices.Index];
|
||||
|
||||
BonesTransform[boneIndices.Index] = new Transform[MaxFrame];
|
||||
for (int frame = 0; frame < BonesTransform[boneIndices.Index].Length; frame++)
|
||||
{
|
||||
var boneOrientation = originalTransform.Rotation;
|
||||
var bonePosition = originalTransform.Position;
|
||||
var boneScale = originalTransform.Scale;
|
||||
|
||||
sequence.Tracks[trackIndex].GetBonePosition(frame, MaxFrame, false, ref bonePosition, ref boneOrientation);
|
||||
if (frame < sequence.Tracks[trackIndex].KeyScale.Length)
|
||||
boneScale = sequence.Tracks[trackIndex].KeyScale[frame];
|
||||
|
||||
// revert FixRotationKeys
|
||||
if (trackIndex > 0) boneOrientation.Conjugate();
|
||||
|
||||
bonePosition *= Constants.SCALE_DOWN_RATIO;
|
||||
|
||||
// switch (boneModes[trackIndex])
|
||||
// {
|
||||
// case EBoneRetargetingMode.Animation:
|
||||
// case EBoneRetargetingMode.Mesh:
|
||||
// case EBoneRetargetingMode.AnimationScaled:
|
||||
// case EBoneRetargetingMode.AnimationRelative:
|
||||
// case EBoneRetargetingMode.OrientAndScale:
|
||||
// case EBoneRetargetingMode.Count:
|
||||
// default:
|
||||
// break;
|
||||
// }
|
||||
|
||||
BonesTransform[boneIndices.Index][frame] = new Transform
|
||||
{
|
||||
Relation = boneIndices.ParentIndex >= 0 ? BonesTransform[boneIndices.ParentIndex][frame].Matrix : originalTransform.Relation,
|
||||
Rotation = boneOrientation,
|
||||
Position = rotationOnly ? originalTransform.Position : bonePosition,
|
||||
Scale = boneScale
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -5,24 +5,31 @@ using CUE4Parse_Conversion.Animations;
|
||||
using CUE4Parse.UE4.Assets.Exports.Animation;
|
||||
using CUE4Parse.UE4.Objects.UObject;
|
||||
using FModel.Views.Snooper.Shading;
|
||||
using Serilog;
|
||||
|
||||
namespace FModel.Views.Snooper.Models.Animations;
|
||||
|
||||
public struct BoneIndice
|
||||
{
|
||||
public int Index;
|
||||
public int ParentIndex;
|
||||
}
|
||||
|
||||
public class Skeleton : IDisposable
|
||||
{
|
||||
public readonly USkeleton UnrealSkeleton;
|
||||
public readonly FReferenceSkeleton ReferenceSkeleton;
|
||||
public readonly Dictionary<string, int> BonesIndexByLoweredName;
|
||||
public readonly bool IsLoaded;
|
||||
|
||||
public readonly Dictionary<string, BoneIndice> BonesIndicesByLoweredName;
|
||||
public readonly Dictionary<int, Transform> BonesTransformByIndex;
|
||||
public readonly Dictionary<int, Matrix4x4> InvertedBonesMatrixByIndex;
|
||||
public readonly bool IsLoaded;
|
||||
|
||||
public Animation Anim;
|
||||
public bool HasAnim => Anim != null;
|
||||
|
||||
public Skeleton()
|
||||
{
|
||||
BonesIndexByLoweredName = new Dictionary<string, int>();
|
||||
BonesIndicesByLoweredName = new Dictionary<string, BoneIndice>();
|
||||
BonesTransformByIndex = new Dictionary<int, Transform>();
|
||||
InvertedBonesMatrixByIndex = new Dictionary<int, Matrix4x4>();
|
||||
}
|
||||
@@ -33,16 +40,25 @@ public class Skeleton : IDisposable
|
||||
IsLoaded = UnrealSkeleton != null;
|
||||
if (!IsLoaded) return;
|
||||
|
||||
ReferenceSkeleton = referenceSkeleton;
|
||||
foreach ((var name, var boneIndex) in referenceSkeleton.FinalNameToIndexMap)
|
||||
BonesIndexByLoweredName[name.ToLower()] = boneIndex;
|
||||
|
||||
foreach (var boneIndex in BonesIndexByLoweredName.Values)
|
||||
for (int boneIndex = 0; boneIndex < referenceSkeleton.FinalRefBoneInfo.Length; boneIndex++)
|
||||
{
|
||||
var bone = ReferenceSkeleton.FinalRefBonePose[boneIndex];
|
||||
var parentIndex = ReferenceSkeleton.FinalRefBoneInfo[boneIndex].ParentIndex;
|
||||
var info = referenceSkeleton.FinalRefBoneInfo[boneIndex];
|
||||
BonesIndicesByLoweredName[info.Name.Text.ToLower()] = new BoneIndice { Index = boneIndex, ParentIndex = info.ParentIndex };
|
||||
}
|
||||
|
||||
if (!BonesTransformByIndex.TryGetValue(boneIndex, out var boneTransform))
|
||||
#if DEBUG
|
||||
for (int trackIndex = 0; trackIndex < UnrealSkeleton.ReferenceSkeleton.FinalRefBoneInfo.Length; trackIndex++)
|
||||
{
|
||||
var bone = UnrealSkeleton.ReferenceSkeleton.FinalRefBoneInfo[trackIndex];
|
||||
if (!BonesIndicesByLoweredName.TryGetValue(bone.Name.Text.ToLower(), out _))
|
||||
Log.Warning($"Bone Mismatch: {bone.Name.Text} ({trackIndex}) is not present in the mesh's skeleton");
|
||||
}
|
||||
#endif
|
||||
|
||||
foreach (var boneIndices in BonesIndicesByLoweredName.Values)
|
||||
{
|
||||
var bone = referenceSkeleton.FinalRefBonePose[boneIndices.Index];
|
||||
if (!BonesTransformByIndex.TryGetValue(boneIndices.Index, out var boneTransform))
|
||||
{
|
||||
boneTransform = new Transform
|
||||
{
|
||||
@@ -52,14 +68,14 @@ public class Skeleton : IDisposable
|
||||
};
|
||||
}
|
||||
|
||||
if (!BonesTransformByIndex.TryGetValue(parentIndex, out var parentTransform))
|
||||
if (!BonesTransformByIndex.TryGetValue(boneIndices.ParentIndex, out var parentTransform))
|
||||
parentTransform = new Transform { Relation = Matrix4x4.Identity };
|
||||
|
||||
boneTransform.Relation = parentTransform.Matrix;
|
||||
Matrix4x4.Invert(boneTransform.Matrix, out var inverted);
|
||||
|
||||
BonesTransformByIndex[boneIndex] = boneTransform;
|
||||
InvertedBonesMatrixByIndex[boneIndex] = inverted;
|
||||
BonesTransformByIndex[boneIndices.Index] = boneTransform;
|
||||
InvertedBonesMatrixByIndex[boneIndices.Index] = inverted;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,21 +84,18 @@ public class Skeleton : IDisposable
|
||||
Anim = new Animation(this, anim, rotationOnly);
|
||||
}
|
||||
|
||||
public void SetPoseUniform(Shader shader)
|
||||
{
|
||||
if (!IsLoaded) return;
|
||||
foreach (var boneIndex in BonesTransformByIndex.Keys)
|
||||
{
|
||||
if (boneIndex >= Constants.MAX_BONE_UNIFORM)
|
||||
break;
|
||||
shader.SetUniform($"uFinalBonesMatrix[{boneIndex}]", Matrix4x4.Identity);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetUniform(Shader shader, float deltaSeconds = 0f, bool update = false)
|
||||
{
|
||||
if (!IsLoaded) return;
|
||||
if (!HasAnim) SetPoseUniform(shader);
|
||||
if (!HasAnim)
|
||||
{
|
||||
foreach (var boneIndex in BonesTransformByIndex.Keys)
|
||||
{
|
||||
if (boneIndex >= Constants.MAX_BONE_UNIFORM)
|
||||
break;
|
||||
shader.SetUniform($"uFinalBonesMatrix[{boneIndex}]", Matrix4x4.Identity);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (update) Anim.Update(deltaSeconds);
|
||||
@@ -92,18 +105,17 @@ public class Skeleton : IDisposable
|
||||
break;
|
||||
if (!InvertedBonesMatrixByIndex.TryGetValue(boneIndex, out var invertMatrix))
|
||||
throw new ArgumentNullException($"no inverse matrix for bone '{boneIndex}'");
|
||||
if (!Anim.TrackIndexByBoneIndex.TryGetValue(boneIndex, out var trackIndex))
|
||||
throw new ArgumentNullException($"no track index for bone '{boneIndex}'");
|
||||
|
||||
shader.SetUniform($"uFinalBonesMatrix[{boneIndex}]", invertMatrix * Anim.InterpolateBoneTransform(trackIndex));
|
||||
shader.SetUniform($"uFinalBonesMatrix[{boneIndex}]", invertMatrix * Anim.InterpolateBoneTransform(boneIndex));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
BonesIndexByLoweredName.Clear();
|
||||
BonesIndicesByLoweredName.Clear();
|
||||
BonesTransformByIndex.Clear();
|
||||
InvertedBonesMatrixByIndex.Clear();
|
||||
Anim?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,11 +252,11 @@ public class Model : IDisposable
|
||||
foreach (var socket in Sockets)
|
||||
{
|
||||
var boneMatrix = Matrix4x4.Identity;
|
||||
if (HasSkeleton && Skeleton.BonesIndexByLoweredName.TryGetValue(socket.BoneName.Text.ToLower(), out var boneIndex))
|
||||
if (HasSkeleton && Skeleton.BonesIndicesByLoweredName.TryGetValue(socket.BoneName.Text.ToLower(), out var boneIndices))
|
||||
{
|
||||
if (Skeleton.Anim?.TrackIndexByBoneIndex.TryGetValue(boneIndex, out var trackIndex) ?? false)
|
||||
boneMatrix = Skeleton.Anim.InterpolateBoneTransform(trackIndex);
|
||||
else if (Skeleton.BonesTransformByIndex.TryGetValue(boneIndex, out var boneTransform))
|
||||
if (Skeleton.HasAnim)
|
||||
boneMatrix = Skeleton.Anim.InterpolateBoneTransform(boneIndices.Index);
|
||||
else if (Skeleton.BonesTransformByIndex.TryGetValue(boneIndices.Index, out var boneTransform))
|
||||
boneMatrix = boneTransform.Matrix;
|
||||
}
|
||||
|
||||
|
||||
@@ -90,13 +90,20 @@ public class Renderer : IDisposable
|
||||
Options.SwapMaterial(false);
|
||||
}
|
||||
|
||||
public void Animate(UAnimSequence animSequence)
|
||||
public void Animate(UObject anim)
|
||||
{
|
||||
if (!Options.TryGetModel(out var model) || !model.Skeleton.IsLoaded ||
|
||||
model.Skeleton?.UnrealSkeleton.ConvertAnims(animSequence) is not { } anim || anim.Sequences.Count == 0)
|
||||
if (!Options.TryGetModel(out var model) || !model.Skeleton.IsLoaded)
|
||||
return;
|
||||
|
||||
model.Skeleton.SetAnimation(anim, AnimateWithRotationOnly);
|
||||
switch (anim)
|
||||
{
|
||||
case UAnimSequence animSequence:
|
||||
model.Skeleton.SetAnimation(model.Skeleton.UnrealSkeleton.ConvertAnims(animSequence), AnimateWithRotationOnly);
|
||||
break;
|
||||
case UAnimMontage animMontage:
|
||||
model.Skeleton.SetAnimation(model.Skeleton.UnrealSkeleton.ConvertAnims(animMontage), AnimateWithRotationOnly);
|
||||
break;
|
||||
}
|
||||
Options.AnimateMesh(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -452,7 +452,7 @@ Snooper aims to give an accurate preview of models, materials, skeletal animatio
|
||||
if (model.HasSkeleton)
|
||||
{
|
||||
Layout("Skeleton");ImGui.Text($" : {model.Skeleton.UnrealSkeleton.Name}");
|
||||
Layout("Bones");ImGui.Text($" : x{model.Skeleton.UnrealSkeleton.BoneTree.Length}");
|
||||
Layout("Bones");ImGui.Text($" : x{model.Skeleton.InvertedBonesMatrixByIndex.Count}");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user