Skip to content

Commit 3489347

Browse files
Merge pull request #226 from EJTP/pp2
improved Animations
2 parents ef3f862 + 8a9d5e5 commit 3489347

4 files changed

Lines changed: 146 additions & 63 deletions

File tree

Prowl.Runtime/AssetImporting/ModelImporter.cs

Lines changed: 50 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,18 @@ private Model BuildModel(Assimp.Scene scene, string assetPath, DirectoryInfo? pa
124124
// Build the model structure
125125
model.RootNode = BuildModelNode(scene.RootNode, scale);
126126

127+
var rootTransform = scene.RootNode.Transform;
128+
Float4x4 rootMatrix = new Float4x4(
129+
rootTransform.A1, rootTransform.A2, rootTransform.A3, rootTransform.A4,
130+
rootTransform.B1, rootTransform.B2, rootTransform.B3, rootTransform.B4,
131+
rootTransform.C1, rootTransform.C2, rootTransform.C3, rootTransform.C4,
132+
rootTransform.D1, rootTransform.D2, rootTransform.D3, rootTransform.D4
133+
);
134+
135+
rootMatrix.Translation *= (float)scale;
136+
137+
model.GlobalInverseTransform = rootMatrix.Invert();
138+
127139
// Load materials and meshes into the model
128140
if (scene.HasMaterials)
129141
LoadMaterials(scene, parentDir, model.Materials);
@@ -132,27 +144,9 @@ private Model BuildModel(Assimp.Scene scene, string assetPath, DirectoryInfo? pa
132144
LoadMeshes(assetPath, settings, scene, scale, model.Materials, model.Meshes);
133145

134146
// Animations
135-
List<AnimationClip> anims = [];
136147
if (scene.HasAnimations)
137148
LoadAnimations(scene, scale, model.Animations);
138149

139-
//if (CullEmpty)
140-
//{
141-
// // Remove Empty GameObjects
142-
// List<(MeshRenderer, Node)> GOsToRemove = [];
143-
// foreach (var go in GOs)
144-
// {
145-
// if (go.Item1.GetEntitiesInChildren<MeshRenderer>().Count(x => x.Mesh.IsAvailable) == 0)
146-
// GOsToRemove.Add(go);
147-
// }
148-
// foreach (var go in GOsToRemove)
149-
// {
150-
// if (!go.Item1.IsDestroyed)
151-
// go.Item1.DestroyImmediate();
152-
// GOs.Remove(go);
153-
// }
154-
//}
155-
156150
return model;
157151
}
158152

@@ -177,7 +171,6 @@ private void LoadMaterials(Assimp.Scene? scene, DirectoryInfo? parentDir, List<M
177171
}
178172
else
179173
{
180-
181174
mat.SetFloat("_EmissionIntensity", 0f);
182175
mat.SetColor("_EmissiveColor", Color.Black);
183176
}
@@ -261,7 +254,6 @@ private void LoadMeshes(string assetPath, ModelImporterSettings settings, Assimp
261254
continue;
262255
}
263256

264-
265257
Mesh mesh = new();
266258
mesh.Name = m.Name;
267259
int vertexCount = m.VertexCount;
@@ -317,10 +309,6 @@ private void LoadMeshes(string assetPath, ModelImporterSettings settings, Assimp
317309
}
318310

319311
mesh.Indices = m.GetUnsignedIndices();
320-
321-
//if(!m.HasTangentBasis)
322-
// mesh.RecalculateTangents();
323-
324312
mesh.RecalculateBounds();
325313

326314
if (m.HasBones)
@@ -329,6 +317,7 @@ private void LoadMeshes(string assetPath, ModelImporterSettings settings, Assimp
329317
mesh.boneNames = new string[m.Bones.Count];
330318
mesh.BoneIndices = new Float4[vertexCount];
331319
mesh.BoneWeights = new Float4[vertexCount];
320+
332321
for (var i = 0; i < m.Bones.Count; i++)
333322
{
334323
var bone = m.Bones[i];
@@ -338,16 +327,13 @@ private void LoadMeshes(string assetPath, ModelImporterSettings settings, Assimp
338327

339328
var offsetMatrix = bone.OffsetMatrix;
340329
Float4x4 bindPose = new Float4x4(
341-
offsetMatrix.A1, offsetMatrix.B1, offsetMatrix.C1, offsetMatrix.D1,
342-
offsetMatrix.A2, offsetMatrix.B2, offsetMatrix.C2, offsetMatrix.D2,
343-
offsetMatrix.A3, offsetMatrix.B3, offsetMatrix.C3, offsetMatrix.D3,
344-
offsetMatrix.A4, offsetMatrix.B4, offsetMatrix.C4, offsetMatrix.D4
330+
offsetMatrix.A1, offsetMatrix.A2, offsetMatrix.A3, offsetMatrix.A4,
331+
offsetMatrix.B1, offsetMatrix.B2, offsetMatrix.B3, offsetMatrix.B4,
332+
offsetMatrix.C1, offsetMatrix.C2, offsetMatrix.C3, offsetMatrix.C4,
333+
offsetMatrix.D1, offsetMatrix.D2, offsetMatrix.D3, offsetMatrix.D4
345334
);
346335

347-
// Adjust translation by scale
348336
bindPose.Translation *= (float)scale;
349-
//var translate = Float4x4.CreateScale((float)scale);
350-
//bindPose = Maths.Mul(translate, bindPose);
351337

352338
mesh.bindPoses[i] = bindPose;
353339

@@ -448,83 +434,87 @@ private static void LoadAnimations(Assimp.Scene? scene, double scale, List<Anima
448434
animation.Duration = anim.DurationInTicks / (anim.TicksPerSecond != 0 ? anim.TicksPerSecond : 25.0);
449435
animation.TicksPerSecond = anim.TicksPerSecond;
450436
animation.DurationInTicks = anim.DurationInTicks;
451-
437+
452438
foreach (var channel in anim.NodeAnimationChannels)
453439
{
454440
Assimp.Node boneNode = scene.RootNode.FindNode(channel.NodeName);
455-
441+
456442
var animBone = new AnimationClip.AnimBone();
457443
animBone.BoneName = boneNode.Name;
458-
459-
// construct full path from RootNode to this bone
460-
// RootNode -> Parent -> Parent -> ... -> Parent -> Bone
461-
Assimp.Node target = boneNode;
462-
string path = target.Name;
463-
//while (target.Parent != null)
464-
//{
465-
// target = target.Parent;
466-
// path = target.Name + "/" + path;
467-
// if (target.Name == scene.RootNode.Name) // TODO: Can we just do reference comparison here instead of string comparison?
468-
// break;
469-
//}
470-
444+
471445
if (channel.HasPositionKeys)
472446
{
473447
var xCurve = new AnimationCurve();
474448
var yCurve = new AnimationCurve();
475449
var zCurve = new AnimationCurve();
450+
451+
xCurve.Keys.Clear();
452+
yCurve.Keys.Clear();
453+
zCurve.Keys.Clear();
454+
476455
foreach (var posKey in channel.PositionKeys)
477456
{
478457
double time = (posKey.Time / anim.DurationInTicks) * animation.Duration;
479-
xCurve.Keys.Add(new(time, posKey.Value.X * scale));
480-
yCurve.Keys.Add(new(time, posKey.Value.Y * scale));
481-
zCurve.Keys.Add(new(time, posKey.Value.Z * scale));
458+
xCurve.Keys.Add(new KeyFrame(time, posKey.Value.X * scale));
459+
yCurve.Keys.Add(new KeyFrame(time, posKey.Value.Y * scale));
460+
zCurve.Keys.Add(new KeyFrame(time, posKey.Value.Z * scale));
482461
}
483462
animBone.PosX = xCurve;
484463
animBone.PosY = yCurve;
485464
animBone.PosZ = zCurve;
486465
}
487-
466+
488467
if (channel.HasRotationKeys)
489468
{
490469
var xCurve = new AnimationCurve();
491470
var yCurve = new AnimationCurve();
492471
var zCurve = new AnimationCurve();
493472
var wCurve = new AnimationCurve();
473+
474+
xCurve.Keys.Clear();
475+
yCurve.Keys.Clear();
476+
zCurve.Keys.Clear();
477+
wCurve.Keys.Clear();
478+
494479
foreach (var rotKey in channel.RotationKeys)
495480
{
496481
double time = (rotKey.Time / anim.DurationInTicks) * animation.Duration;
497-
xCurve.Keys.Add(new(time, rotKey.Value.X));
498-
yCurve.Keys.Add(new(time, rotKey.Value.Y));
499-
zCurve.Keys.Add(new(time, rotKey.Value.Z));
500-
wCurve.Keys.Add(new(time, rotKey.Value.W));
482+
xCurve.Keys.Add(new KeyFrame(time, rotKey.Value.X));
483+
yCurve.Keys.Add(new KeyFrame(time, rotKey.Value.Y));
484+
zCurve.Keys.Add(new KeyFrame(time, rotKey.Value.Z));
485+
wCurve.Keys.Add(new KeyFrame(time, rotKey.Value.W));
501486
}
502487
animBone.RotX = xCurve;
503488
animBone.RotY = yCurve;
504489
animBone.RotZ = zCurve;
505490
animBone.RotW = wCurve;
506491
}
507-
492+
508493
if (channel.HasScalingKeys)
509494
{
510495
var xCurve = new AnimationCurve();
511496
var yCurve = new AnimationCurve();
512497
var zCurve = new AnimationCurve();
498+
499+
xCurve.Keys.Clear();
500+
yCurve.Keys.Clear();
501+
zCurve.Keys.Clear();
502+
513503
foreach (var scaleKey in channel.ScalingKeys)
514504
{
515505
double time = (scaleKey.Time / anim.DurationInTicks) * animation.Duration;
516-
xCurve.Keys.Add(new(time, scaleKey.Value.X));
517-
yCurve.Keys.Add(new(time, scaleKey.Value.Y));
518-
zCurve.Keys.Add(new(time, scaleKey.Value.Z));
506+
xCurve.Keys.Add(new KeyFrame(time, scaleKey.Value.X));
507+
yCurve.Keys.Add(new KeyFrame(time, scaleKey.Value.Y));
508+
zCurve.Keys.Add(new KeyFrame(time, scaleKey.Value.Z));
519509
}
520510
animBone.ScaleX = xCurve;
521511
animBone.ScaleY = yCurve;
522512
animBone.ScaleZ = zCurve;
523513
}
524-
514+
525515
animation.AddBone(animBone);
526516
}
527-
517+
528518
animation.EnsureQuaternionContinuity();
529519
animations.Add(animation);
530520
}

Prowl.Runtime/Components/ModelRenderer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public class ModelRenderer : MonoBehaviour
1919
public AnimationClip CurrentAnimation;
2020
public bool PlayAutomatically = true;
2121
public bool Loop = true;
22-
public double AnimationSpeed = 0.1;
22+
public double AnimationSpeed = 10.0;
2323

2424
private double _animationTime = 0.0;
2525
private bool _isPlaying = false;

Prowl.Runtime/Resources/AnimationClip.cs

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// This file is part of the Prowl Game Engine
22
// Licensed under the MIT License. See the LICENSE file in the project root for details.
33

4+
using System;
45
using System.Collections.Generic;
56
using System.Linq;
67

@@ -164,11 +165,100 @@ public Double3 EvaluatePositionAt(double time)
164165
=> new Double3(PosX.Evaluate(time), PosY.Evaluate(time), PosZ.Evaluate(time));
165166

166167
public Quaternion EvaluateRotationAt(double time)
167-
=> new Quaternion((float)RotX.Evaluate(time), (float)RotY.Evaluate(time), (float)RotZ.Evaluate(time), (float)RotW.Evaluate(time));
168+
{
169+
// Use SLERP for smooth quaternion interpolation
170+
if (RotX.Keys.Count == 0)
171+
return Quaternion.Identity;
172+
173+
if (RotX.Keys.Count == 1)
174+
{
175+
return NormalizeQuaternion(new Quaternion(
176+
(float)RotX.Keys[0].Value,
177+
(float)RotY.Keys[0].Value,
178+
(float)RotZ.Keys[0].Value,
179+
(float)RotW.Keys[0].Value
180+
));
181+
}
182+
183+
// Find the two keyframes to interpolate between
184+
int idx0 = -1;
185+
int idx1 = -1;
186+
187+
for (int i = 0; i < RotX.Keys.Count - 1; i++)
188+
{
189+
if (time >= RotX.Keys[i].Position && time <= RotX.Keys[i + 1].Position)
190+
{
191+
idx0 = i;
192+
idx1 = i + 1;
193+
break;
194+
}
195+
}
196+
197+
// Handle edge cases
198+
if (idx0 == -1)
199+
{
200+
if (time <= RotX.Keys[0].Position)
201+
{
202+
idx0 = idx1 = 0;
203+
}
204+
else if (time >= RotX.Keys[^1].Position)
205+
{
206+
idx0 = idx1 = RotX.Keys.Count - 1;
207+
}
208+
else
209+
{
210+
// Shouldn't happen, but fallback to first frame
211+
idx0 = idx1 = 0;
212+
}
213+
}
214+
215+
var key0 = RotX.Keys[idx0];
216+
var key1 = RotX.Keys[idx1];
217+
218+
Quaternion q0 = new (
219+
(float)RotX.Keys[idx0].Value,
220+
(float)RotY.Keys[idx0].Value,
221+
(float)RotZ.Keys[idx0].Value,
222+
(float)RotW.Keys[idx0].Value
223+
);
224+
225+
Quaternion q1 = new (
226+
(float)RotX.Keys[idx1].Value,
227+
(float)RotY.Keys[idx1].Value,
228+
(float)RotZ.Keys[idx1].Value,
229+
(float)RotW.Keys[idx1].Value
230+
);
231+
232+
float t = 0;
233+
if (key1.Position != key0.Position)
234+
{
235+
t = (float)((time - key0.Position) / (key1.Position - key0.Position));
236+
t = Math.Clamp(t, 0f, 1f);
237+
}
238+
239+
return Maths.Slerp(q0, q1, t);
240+
}
168241

169242
public Double3 EvaluateScaleAt(double time)
170243
=> new Double3(ScaleX.Evaluate(time), ScaleY.Evaluate(time), ScaleZ.Evaluate(time));
171-
}
172244

245+
/// <summary>
246+
/// Normalize a quaternion
247+
/// </summary>
248+
private static Quaternion NormalizeQuaternion(Quaternion q)
249+
{
250+
float length = (float)Math.Sqrt(q.X * q.X + q.Y * q.Y + q.Z * q.Z + q.W * q.W);
251+
252+
if (length < 0.0001f)
253+
return Quaternion.Identity;
173254

255+
float invLength = 1.0f / length;
256+
return new Quaternion(
257+
q.X * invLength,
258+
q.Y * invLength,
259+
q.Z * invLength,
260+
q.W * invLength
261+
);
262+
}
263+
}
174264
}

Prowl.Runtime/Resources/Model.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ public class Model : EngineObject
1515
public List<AnimationClip> Animations { get; set; } = new();
1616
public float UnitScale { get; set; } = 1.0f;
1717

18+
// This transforms from world space back to mesh/model space
19+
public Float4x4 GlobalInverseTransform { get; set; } = Float4x4.Identity;
20+
1821
public Model(string name)
1922
{
2023
Name = name;

0 commit comments

Comments
 (0)