Start of AnimationSystem

This commit is contained in:
Simon Lübeß
2021-08-27 22:37:59 +02:00
parent eafdc2f17f
commit ce03f7c600
8 changed files with 707 additions and 29 deletions
+194 -3
View File
@@ -3,13 +3,19 @@ using System.Collections;
using cgltf;
using GlitchyEngine.Math;
using GlitchyEngine.Renderer;
using GlitchyEngine.Renderer.Animation;
namespace GlitchyEngine.Content
{
public static class ModelLoader
{
public static void LoadModel(String filename, GraphicsContext context, Effect validationEffect, List<(Matrix Transform, GeometryBinding Model)> output)
static readonly Matrix RightToLeftHand = .Scaling(1, 1, -1);
public static void LoadModel(String filename, GraphicsContext context, Effect validationEffect, List<(Matrix Transform, GeometryBinding Model)> output, out Skeleton skeleton, out AnimationClip clip)
{
skeleton = null;
clip = null;
CGLTF.Options options = .();
CGLTF.Data* data;
CGLTF.Result result = CGLTF.ParseFile(options, filename, out data);
@@ -25,7 +31,9 @@ namespace GlitchyEngine.Content
if(node.Mesh != null)
{
Matrix transform = ?;
CGLTF.NodeTransformWorld(&node, (float*)&transform);
CGLTF.NodeTransformLocal(&node, (float*)&transform);
transform = RightToLeftHand * transform;
for(var primitive in node.Mesh.Primitives)
{
@@ -34,6 +42,11 @@ namespace GlitchyEngine.Content
output.Add((transform, binding));
}
}
if(node.Skin != null)
{
(skeleton, clip) = ExtractBonestuff(node.Skin, data);
}
}
/* TODO make awesome stuff */
@@ -96,6 +109,8 @@ namespace GlitchyEngine.Content
VertexBuffer vertexBuffer = null;
binding.VertexCount = (uint32)attribute.Data.Count;
// Get vertex buffer
{
CGLTF.BufferView* bufferView = attribute.Data.BufferView;
@@ -130,7 +145,14 @@ namespace GlitchyEngine.Content
binding.SetVertexBufferSlot(bufferBinding, (.)bindingSlot);
}
VertexElement element = .(format, new String(attribute.Name), true, (.)attribute.Index, (.)bindingSlot);
StringView strView = .(attribute.Name);
while((*(strView.EndPtr - 1)).IsDigit)
{
strView.Length--;
}
VertexElement element = .(format, new String(strView), true, (.)attribute.Index, (.)bindingSlot);
elements.Add(element);
}
@@ -146,9 +168,178 @@ namespace GlitchyEngine.Content
binding.SetVertexLayout(layout..ReleaseRefNoDelete());
}
// TODO: calculate normals if missing
// TODO: calculate tangents if missing (MikkTSpace algorithm)
// Note: Bitangent = cross(normal, tangent.xyz) * tangent.w
return binding;
}
static (Skeleton, AnimationClip) ExtractBonestuff(CGLTF.Skin* skin, CGLTF.Data* data)
{
Skeleton skeleton = new Skeleton();
skeleton.Joints = new Joint[skin.Joints.Length];
AnimationClip testClip = new AnimationClip();
testClip.Skeleton = skeleton;
testClip.FramesPerSecond = 0;
testClip.JointAnimations = new JointAnimation[skeleton.Joints.Count];
//testClip.Samples = new AnimationSample[1];
//testClip.Samples[0].JointPose = new JointPose[skeleton.Joints.Count];
testClip.IsLooping = true;
for(int i < skin.Joints.Length)
{
ref Joint joint = ref skeleton.Joints[i];
joint.InverseBindPose = GetEntry<Matrix>(skin.InverseBindMatrices, i);
joint.Name = new String(skin.Joints[i].Name);
int parentId = skin.Joints.IndexOf(skin.Joints[i].Parent);
Log.EngineLogger.AssertDebug(parentId < uint8.MaxValue, scope $"A skeleton must not have more than {uint8.MaxValue - 1} bones.");
if(parentId == -1)
joint.ParentID = uint8.MaxValue;
else
joint.ParentID = (uint8)parentId;
}
for(var channel in data.Animations[0].Channels)
{
int nodeIndex = skin.Joints.IndexOf(channel.TargetNode);
Log.EngineLogger.AssertDebug(nodeIndex != -1);
ref JointAnimation jointAnimation = ref testClip.JointAnimations[nodeIndex];
Log.EngineLogger.AssertDebug(channel.Sampler.Input.Count == channel.Sampler.Output.Count);
int samples = (int)channel.Sampler.Input.Count;
Log.EngineLogger.AssertDebug(channel.Sampler.Input.ComponentType == .R_32f);
Log.EngineLogger.AssertDebug(channel.Sampler.Input.Type == .Scalar);
InterpolationMode interpolationMode;
switch(channel.Sampler.Interpolation)
{
case .Step:
interpolationMode = .Step;
case .Linear:
interpolationMode = .Linear;
case .CubicSpline:
interpolationMode = .CubicSpline;
}
switch(channel.TargetPath)
{
case .Translation:
jointAnimation.TranslationChannel = new .(samples, interpolationMode);
Log.EngineLogger.AssertDebug(channel.Sampler.Output.ComponentType == .R_32f);
Log.EngineLogger.AssertDebug(channel.Sampler.Output.Type == .Vec3);
for(int i < samples)
{
float timeStamp = GetEntry<float>(channel.Sampler.Input, i);
Vector3 sample = GetEntry<Vector3>(channel.Sampler.Output, i);
/*
float timeStamp = GetEntry<float>(channel.Sampler.Input, i);
float timeStamp2 = ?;
CGLTF.AccessorReadFloat(channel.Sampler.Input, (uint)i, (float*)&timeStamp2, 1);
Log.EngineLogger.Assert(timeStamp == timeStamp2);
Vector3 sample2 = ?;
CGLTF.AccessorReadFloat(channel.Sampler.Output, (uint)i, (float*)&sample2, 3);
Log.EngineLogger.Assert(sample == sample2);
*/
jointAnimation.TranslationChannel.TimeStamps[i] = timeStamp;
jointAnimation.TranslationChannel.Values[i] = sample;
}
case .Rotation:
jointAnimation.RotationChannel = new .(samples, interpolationMode);
Log.EngineLogger.AssertDebug(channel.Sampler.Output.ComponentType == .R_32f);
Log.EngineLogger.AssertDebug(channel.Sampler.Output.Type == .Vec4);
for(int i < samples)
{
float timeStamp = GetEntry<float>(channel.Sampler.Input, i);
Quaternion sample = GetEntry<Quaternion>(channel.Sampler.Output, i);
jointAnimation.RotationChannel.TimeStamps[i] = timeStamp;
jointAnimation.RotationChannel.Values[i] = sample;
}
case .Scale:
jointAnimation.ScaleChannel = new .(samples, interpolationMode);
Log.EngineLogger.AssertDebug(channel.Sampler.Output.ComponentType == .R_32f);
Log.EngineLogger.AssertDebug(channel.Sampler.Output.Type == .Vec3);
for(int i < samples)
{
float timeStamp = GetEntry<float>(channel.Sampler.Input, i);
Vector3 sample = GetEntry<Vector3>(channel.Sampler.Output, i);
jointAnimation.ScaleChannel.TimeStamps[i] = timeStamp;
jointAnimation.ScaleChannel.Values[i] = sample;
}
default:
Log.EngineLogger.Error($"Unknown channel target path \"{channel.TargetPath}\"");
}
//channel.
//
//testClip.JointAnimations[i].
testClip.Duration = Math.Max(testClip.Duration, jointAnimation.Duration);
}
return (skeleton, testClip);
}
static T GetEntry<T>(CGLTF.Accessor* accessor, int index)
{
Log.EngineLogger.AssertDebug((uint)index < accessor.Count);
T result = ?;
switch(typeof(T))
{
case typeof(float):
CGLTF.AccessorReadFloat(accessor, (uint)index, (float*)&result, 1);
case typeof(Vector2):
CGLTF.AccessorReadFloat(accessor, (uint)index, (float*)&result, 2);
case typeof(Vector3):
CGLTF.AccessorReadFloat(accessor, (uint)index, (float*)&result, 3);
case typeof(Vector4), typeof(Quaternion):
CGLTF.AccessorReadFloat(accessor, (uint)index, (float*)&result, 4);
case typeof(Matrix):
CGLTF.AccessorReadFloat(accessor, (uint)index, (float*)&result, 16);
}
return result;
/*
uint8* data = (uint8*)accessor.BufferView.Buffer.Data;
data += accessor.BufferView.Offset;
data += accessor.Offset;
data += accessor.Stride * (uint)index;
return *(T*)data;
*/
}
/**
* Converts the vector and component type to the corresponding Format.
*/
@@ -0,0 +1,258 @@
using GlitchyEngine.Math;
using System;
namespace GlitchyEngine.Renderer.Animation
{
class AnimationPlayer
{
public Skeleton Skeleton;
public AnimationClip CurrentClip;
public float TimeStamp;
public SkeletonPose Pose ~ delete _;
public Matrix[] SkinningMatricies ~ delete _;
public Matrix3x3[] InvTransSkinningMatricies ~ delete _;
public this(Skeleton skeleton, AnimationClip clip)
{
Skeleton = skeleton;
CurrentClip = clip;
Pose = new SkeletonPose(Skeleton);
SkinningMatricies = new Matrix[Skeleton.Joints.Count];
InvTransSkinningMatricies = new Matrix3x3[Skeleton.Joints.Count];
}
public void Update(GameTime gameTime)
{
TimeStamp += (float)gameTime.FrameTime.TotalSeconds;
if(CurrentClip.IsLooping && CurrentClip.Duration != 0)
{
TimeStamp %= CurrentClip.Duration;
}
for(int i < Skeleton.Joints.Count)
{
ref JointPose localPose = ref Pose.LocalPose[i];
localPose = CurrentClip.JointAnimations[i].GetCurrentPose(TimeStamp);
Matrix jointToParent =
Matrix.Translation(localPose.Translation) *
Matrix.RotationQuaternion(localPose.Rotation) *
Matrix.Scaling(localPose.Scale);
uint8 parentIndex = Skeleton.Joints[i].ParentID;
ref Matrix globalPose = ref Pose.GlobalPose[i];
if(parentIndex == uint8.MaxValue)
{
globalPose = jointToParent;
}
else
{
globalPose = Pose.GlobalPose[parentIndex] * jointToParent;
}
SkinningMatricies[i] = globalPose * Skeleton.Joints[i].InverseBindPose;
InvTransSkinningMatricies[i] = ((Matrix3x3)SkinningMatricies[i]).Inverse().Transpose();
}
}
}
class AnimationClip
{
public Skeleton Skeleton;
public float FramesPerSecond;
public JointAnimation[] JointAnimations;
public bool IsLooping;
public float Duration;
// TODO: check
public ~this()
{
for(var jointAnimation in JointAnimations)
{
jointAnimation.Dispose();
}
delete JointAnimations;
}
}
public enum InterpolationMode
{
Step,
Linear,
CubicSpline
}
class JointAnimationChannel<T> where T : struct // : IDisposable
{
public float[] TimeStamps;
public T[] Values;
public InterpolationMode InterpolationMode = .Step;
public float Duration => TimeStamps[TimeStamps.Count - 1];
public this(int samples, InterpolationMode interpolationMode)
{
TimeStamps = new float[samples];
Values = new T[samples];
InterpolationMode = interpolationMode;
}
public ~this()
{
delete TimeStamps;
delete Values;
}
public (T, T, float) GetSample(float currentTime)
{
let (previousIndex, previousTime) = FindPreviousTimeStamp(currentTime);
T previousSample = Values[previousIndex];
if(InterpolationMode == .Step)
{
return (previousSample, default(T), 0f);
}
else
{
int nextIndex = previousIndex + 1;
if(nextIndex >= Values.Count)
nextIndex = Values.Count - 1;
float nextTime = TimeStamps[nextIndex];
T nextSample = Values[nextIndex];
float interpolationValue = (currentTime - previousTime) / (nextTime - previousTime);
return (previousSample, nextSample, interpolationValue);
}
}
public (int Index, float timeStamp) FindPreviousTimeStamp(float currentTime)
{
// TODO use binary search to speed things up?
int index = TimeStamps.Count - 1;
for(int i < TimeStamps.Count)
{
if(TimeStamps[i] > currentTime)
{
index = i - 1;
break;
}
}
if(index == -1)
index = 0;
// Don't return -1 but return first frame
return (index, TimeStamps[index]);
//return array.Count - 1;
}
}
struct JointAnimation : IDisposable
{
public JointAnimationChannel<Vector3> TranslationChannel;
public JointAnimationChannel<Quaternion> RotationChannel;
public JointAnimationChannel<Vector3> ScaleChannel;
public float Duration
{
get
{
return Math.Max(Math.Max(
TranslationChannel?.Duration ?? 0, RotationChannel?.Duration ?? 0), ScaleChannel?.Duration ?? 0);
}
}
public void Dispose()
{
delete TranslationChannel;
delete RotationChannel;
delete ScaleChannel;
}
public JointPose GetCurrentPose(float timeStamp)
{
JointPose result;
if(TranslationChannel != null)
{
let (previous, next, interpolationValue) = TranslationChannel.GetSample(timeStamp);
switch(TranslationChannel.InterpolationMode)
{
case .Step:
result.Translation = previous;
case .Linear:
result.Translation = Vector3.Lerp(previous, next, interpolationValue);
default:
result.Rotation = ?;
Runtime.NotImplemented();
}
}
else
{
result.Translation = .Zero;
}
if(RotationChannel != null)
{
let (previous, next, interpolationValue) = RotationChannel.GetSample(timeStamp);
switch(RotationChannel.InterpolationMode)
{
case .Step:
result.Rotation = previous;
case .Linear:
result.Rotation = Quaternion.Slerp(previous, next, interpolationValue);
default:
result.Rotation = ?;
Runtime.NotImplemented();
}
}
else
{
result.Rotation = .Identity;
}
if(ScaleChannel != null)
{
let (previous, next, interpolationValue) = ScaleChannel.GetSample(timeStamp);
//switch(ScaleChannel.InterpolationMode)
switch(InterpolationMode.Step)
{
case .Step:
result.Scale = previous;
case .Linear:
result.Scale = Vector3.Lerp(previous, next, interpolationValue);
default:
result.Rotation = ?;
Runtime.NotImplemented();
}
}
else
{
result.Scale = .One;
}
return result;
}
}
}
@@ -0,0 +1,20 @@
using GlitchyEngine.Math;
using System;
namespace GlitchyEngine.Renderer.Animation
{
public struct Joint
{
/// Converts vertices from model space to joint space
public Matrix InverseBindPose;
public String Name;
public uint8 ParentID;
}
public struct JointPose
{
public Quaternion Rotation;
public Vector3 Translation;
public Vector3 Scale;
}
}
@@ -0,0 +1,42 @@
using GlitchyEngine.Math;
namespace GlitchyEngine.Renderer.Animation
{
public class Skeleton
{
public Joint[] Joints ~ delete _;
public Joint? GetParent(Joint joint)
{
if(joint.ParentID == uint8.MaxValue)
return null;
return Joints[joint.ParentID];
}
public ~this()
{
for(var joint in Joints)
{
delete joint.Name;
}
}
}
public class SkeletonPose
{
public Skeleton Skeleton;
public JointPose[] LocalPose ~ delete _;
public Matrix[] GlobalPose ~ delete _;
public this(Skeleton skeleton)
{
Skeleton = skeleton;
int jointCount = Skeleton.Joints.Count;
LocalPose = new JointPose[jointCount];
GlobalPose = new Matrix[jointCount];
}
}
}
@@ -139,6 +139,15 @@ namespace GlitchyEngine.Renderer
*(Matrix4x3*)firstByte = value;
}
public void SetData(Matrix4x3[] value)
{
EnsureTypeMatch(4, 3, .Float);
// TODO: assert length
Internal.MemCpy(firstByte, value.Ptr, sizeof(Matrix4x3) * Math.Min(value.Count, _elements));
}
public void SetData(Matrix3x3 value)
{
EnsureTypeMatch(3, 3, .Float);
@@ -147,6 +156,18 @@ namespace GlitchyEngine.Renderer
// Todo: maybe manual copy
}
public void SetData(Matrix3x3[] value)
{
EnsureTypeMatch(3, 3, .Float);
// TODO: assert length
for(int i < Math.Min(value.Count, _elements))
{
((Matrix4x3*)firstByte)[i] = Matrix4x3(value[i]);
}
}
public void SetData(Matrix value)
{