diff --git a/Prowl.Runtime/AssetImporting/ModelImporter.cs b/Prowl.Runtime/AssetImporting/ModelImporter.cs index 1985f026..cbf61eab 100644 --- a/Prowl.Runtime/AssetImporting/ModelImporter.cs +++ b/Prowl.Runtime/AssetImporting/ModelImporter.cs @@ -124,6 +124,18 @@ private Model BuildModel(Assimp.Scene scene, string assetPath, DirectoryInfo? pa // Build the model structure model.RootNode = BuildModelNode(scene.RootNode, scale); + var rootTransform = scene.RootNode.Transform; + Float4x4 rootMatrix = new Float4x4( + rootTransform.A1, rootTransform.A2, rootTransform.A3, rootTransform.A4, + rootTransform.B1, rootTransform.B2, rootTransform.B3, rootTransform.B4, + rootTransform.C1, rootTransform.C2, rootTransform.C3, rootTransform.C4, + rootTransform.D1, rootTransform.D2, rootTransform.D3, rootTransform.D4 + ); + + rootMatrix.Translation *= (float)scale; + + model.GlobalInverseTransform = rootMatrix.Invert(); + // Load materials and meshes into the model if (scene.HasMaterials) LoadMaterials(scene, parentDir, model.Materials); @@ -132,27 +144,9 @@ private Model BuildModel(Assimp.Scene scene, string assetPath, DirectoryInfo? pa LoadMeshes(assetPath, settings, scene, scale, model.Materials, model.Meshes); // Animations - List anims = []; if (scene.HasAnimations) LoadAnimations(scene, scale, model.Animations); - //if (CullEmpty) - //{ - // // Remove Empty GameObjects - // List<(MeshRenderer, Node)> GOsToRemove = []; - // foreach (var go in GOs) - // { - // if (go.Item1.GetEntitiesInChildren().Count(x => x.Mesh.IsAvailable) == 0) - // GOsToRemove.Add(go); - // } - // foreach (var go in GOsToRemove) - // { - // if (!go.Item1.IsDestroyed) - // go.Item1.DestroyImmediate(); - // GOs.Remove(go); - // } - //} - return model; } @@ -177,7 +171,6 @@ private void LoadMaterials(Assimp.Scene? scene, DirectoryInfo? parentDir, List Parent -> Parent -> ... -> Parent -> Bone - Assimp.Node target = boneNode; - string path = target.Name; - //while (target.Parent != null) - //{ - // target = target.Parent; - // path = target.Name + "/" + path; - // if (target.Name == scene.RootNode.Name) // TODO: Can we just do reference comparison here instead of string comparison? - // break; - //} - + if (channel.HasPositionKeys) { var xCurve = new AnimationCurve(); var yCurve = new AnimationCurve(); var zCurve = new AnimationCurve(); + + xCurve.Keys.Clear(); + yCurve.Keys.Clear(); + zCurve.Keys.Clear(); + foreach (var posKey in channel.PositionKeys) { double time = (posKey.Time / anim.DurationInTicks) * animation.Duration; - xCurve.Keys.Add(new(time, posKey.Value.X * scale)); - yCurve.Keys.Add(new(time, posKey.Value.Y * scale)); - zCurve.Keys.Add(new(time, posKey.Value.Z * scale)); + xCurve.Keys.Add(new KeyFrame(time, posKey.Value.X * scale)); + yCurve.Keys.Add(new KeyFrame(time, posKey.Value.Y * scale)); + zCurve.Keys.Add(new KeyFrame(time, posKey.Value.Z * scale)); } animBone.PosX = xCurve; animBone.PosY = yCurve; animBone.PosZ = zCurve; } - + if (channel.HasRotationKeys) { var xCurve = new AnimationCurve(); var yCurve = new AnimationCurve(); var zCurve = new AnimationCurve(); var wCurve = new AnimationCurve(); + + xCurve.Keys.Clear(); + yCurve.Keys.Clear(); + zCurve.Keys.Clear(); + wCurve.Keys.Clear(); + foreach (var rotKey in channel.RotationKeys) { double time = (rotKey.Time / anim.DurationInTicks) * animation.Duration; - xCurve.Keys.Add(new(time, rotKey.Value.X)); - yCurve.Keys.Add(new(time, rotKey.Value.Y)); - zCurve.Keys.Add(new(time, rotKey.Value.Z)); - wCurve.Keys.Add(new(time, rotKey.Value.W)); + xCurve.Keys.Add(new KeyFrame(time, rotKey.Value.X)); + yCurve.Keys.Add(new KeyFrame(time, rotKey.Value.Y)); + zCurve.Keys.Add(new KeyFrame(time, rotKey.Value.Z)); + wCurve.Keys.Add(new KeyFrame(time, rotKey.Value.W)); } animBone.RotX = xCurve; animBone.RotY = yCurve; animBone.RotZ = zCurve; animBone.RotW = wCurve; } - + if (channel.HasScalingKeys) { var xCurve = new AnimationCurve(); var yCurve = new AnimationCurve(); var zCurve = new AnimationCurve(); + + xCurve.Keys.Clear(); + yCurve.Keys.Clear(); + zCurve.Keys.Clear(); + foreach (var scaleKey in channel.ScalingKeys) { double time = (scaleKey.Time / anim.DurationInTicks) * animation.Duration; - xCurve.Keys.Add(new(time, scaleKey.Value.X)); - yCurve.Keys.Add(new(time, scaleKey.Value.Y)); - zCurve.Keys.Add(new(time, scaleKey.Value.Z)); + xCurve.Keys.Add(new KeyFrame(time, scaleKey.Value.X)); + yCurve.Keys.Add(new KeyFrame(time, scaleKey.Value.Y)); + zCurve.Keys.Add(new KeyFrame(time, scaleKey.Value.Z)); } animBone.ScaleX = xCurve; animBone.ScaleY = yCurve; animBone.ScaleZ = zCurve; } - + animation.AddBone(animBone); } - + animation.EnsureQuaternionContinuity(); animations.Add(animation); } diff --git a/Prowl.Runtime/Components/ModelRenderer.cs b/Prowl.Runtime/Components/ModelRenderer.cs index 2ad6c857..86d2992b 100644 --- a/Prowl.Runtime/Components/ModelRenderer.cs +++ b/Prowl.Runtime/Components/ModelRenderer.cs @@ -19,7 +19,7 @@ public class ModelRenderer : MonoBehaviour public AnimationClip CurrentAnimation; public bool PlayAutomatically = true; public bool Loop = true; - public double AnimationSpeed = 0.1; + public double AnimationSpeed = 10.0; private double _animationTime = 0.0; private bool _isPlaying = false; diff --git a/Prowl.Runtime/Resources/AnimationClip.cs b/Prowl.Runtime/Resources/AnimationClip.cs index e8e35a76..fa393feb 100644 --- a/Prowl.Runtime/Resources/AnimationClip.cs +++ b/Prowl.Runtime/Resources/AnimationClip.cs @@ -1,6 +1,7 @@ // This file is part of the Prowl Game Engine // Licensed under the MIT License. See the LICENSE file in the project root for details. +using System; using System.Collections.Generic; using System.Linq; @@ -164,11 +165,100 @@ public Double3 EvaluatePositionAt(double time) => new Double3(PosX.Evaluate(time), PosY.Evaluate(time), PosZ.Evaluate(time)); public Quaternion EvaluateRotationAt(double time) - => new Quaternion((float)RotX.Evaluate(time), (float)RotY.Evaluate(time), (float)RotZ.Evaluate(time), (float)RotW.Evaluate(time)); + { + // Use SLERP for smooth quaternion interpolation + if (RotX.Keys.Count == 0) + return Quaternion.Identity; + + if (RotX.Keys.Count == 1) + { + return NormalizeQuaternion(new Quaternion( + (float)RotX.Keys[0].Value, + (float)RotY.Keys[0].Value, + (float)RotZ.Keys[0].Value, + (float)RotW.Keys[0].Value + )); + } + + // Find the two keyframes to interpolate between + int idx0 = -1; + int idx1 = -1; + + for (int i = 0; i < RotX.Keys.Count - 1; i++) + { + if (time >= RotX.Keys[i].Position && time <= RotX.Keys[i + 1].Position) + { + idx0 = i; + idx1 = i + 1; + break; + } + } + + // Handle edge cases + if (idx0 == -1) + { + if (time <= RotX.Keys[0].Position) + { + idx0 = idx1 = 0; + } + else if (time >= RotX.Keys[^1].Position) + { + idx0 = idx1 = RotX.Keys.Count - 1; + } + else + { + // Shouldn't happen, but fallback to first frame + idx0 = idx1 = 0; + } + } + + var key0 = RotX.Keys[idx0]; + var key1 = RotX.Keys[idx1]; + + Quaternion q0 = new ( + (float)RotX.Keys[idx0].Value, + (float)RotY.Keys[idx0].Value, + (float)RotZ.Keys[idx0].Value, + (float)RotW.Keys[idx0].Value + ); + + Quaternion q1 = new ( + (float)RotX.Keys[idx1].Value, + (float)RotY.Keys[idx1].Value, + (float)RotZ.Keys[idx1].Value, + (float)RotW.Keys[idx1].Value + ); + + float t = 0; + if (key1.Position != key0.Position) + { + t = (float)((time - key0.Position) / (key1.Position - key0.Position)); + t = Math.Clamp(t, 0f, 1f); + } + + return Maths.Slerp(q0, q1, t); + } public Double3 EvaluateScaleAt(double time) => new Double3(ScaleX.Evaluate(time), ScaleY.Evaluate(time), ScaleZ.Evaluate(time)); - } + /// + /// Normalize a quaternion + /// + private static Quaternion NormalizeQuaternion(Quaternion q) + { + float length = (float)Math.Sqrt(q.X * q.X + q.Y * q.Y + q.Z * q.Z + q.W * q.W); + + if (length < 0.0001f) + return Quaternion.Identity; + float invLength = 1.0f / length; + return new Quaternion( + q.X * invLength, + q.Y * invLength, + q.Z * invLength, + q.W * invLength + ); + } + } } diff --git a/Prowl.Runtime/Resources/Model.cs b/Prowl.Runtime/Resources/Model.cs index 45023f5a..d0d52204 100644 --- a/Prowl.Runtime/Resources/Model.cs +++ b/Prowl.Runtime/Resources/Model.cs @@ -15,6 +15,9 @@ public class Model : EngineObject public List Animations { get; set; } = new(); public float UnitScale { get; set; } = 1.0f; + // This transforms from world space back to mesh/model space + public Float4x4 GlobalInverseTransform { get; set; } = Float4x4.Identity; + public Model(string name) { Name = name;