Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 50 additions & 60 deletions Prowl.Runtime/AssetImporting/ModelImporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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<AnimationClip> 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<MeshRenderer>().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;
}

Expand All @@ -177,7 +171,6 @@ private void LoadMaterials(Assimp.Scene? scene, DirectoryInfo? parentDir, List<M
}
else
{

mat.SetFloat("_EmissionIntensity", 0f);
mat.SetColor("_EmissiveColor", Color.black);
}
Expand Down Expand Up @@ -261,7 +254,6 @@ private void LoadMeshes(string assetPath, ModelImporterSettings settings, Assimp
continue;
}


Mesh mesh = new();
mesh.Name = m.Name;
int vertexCount = m.VertexCount;
Expand Down Expand Up @@ -317,10 +309,6 @@ private void LoadMeshes(string assetPath, ModelImporterSettings settings, Assimp
}

mesh.Indices = m.GetUnsignedIndices();

//if(!m.HasTangentBasis)
// mesh.RecalculateTangents();

mesh.RecalculateBounds();

if (m.HasBones)
Expand All @@ -329,6 +317,7 @@ private void LoadMeshes(string assetPath, ModelImporterSettings settings, Assimp
mesh.boneNames = new string[m.Bones.Count];
mesh.BoneIndices = new Float4[vertexCount];
mesh.BoneWeights = new Float4[vertexCount];

for (var i = 0; i < m.Bones.Count; i++)
{
var bone = m.Bones[i];
Expand All @@ -338,16 +327,13 @@ private void LoadMeshes(string assetPath, ModelImporterSettings settings, Assimp

var offsetMatrix = bone.OffsetMatrix;
Float4x4 bindPose = new Float4x4(
offsetMatrix.A1, offsetMatrix.B1, offsetMatrix.C1, offsetMatrix.D1,
offsetMatrix.A2, offsetMatrix.B2, offsetMatrix.C2, offsetMatrix.D2,
offsetMatrix.A3, offsetMatrix.B3, offsetMatrix.C3, offsetMatrix.D3,
offsetMatrix.A4, offsetMatrix.B4, offsetMatrix.C4, offsetMatrix.D4
offsetMatrix.A1, offsetMatrix.A2, offsetMatrix.A3, offsetMatrix.A4,
offsetMatrix.B1, offsetMatrix.B2, offsetMatrix.B3, offsetMatrix.B4,
offsetMatrix.C1, offsetMatrix.C2, offsetMatrix.C3, offsetMatrix.C4,
offsetMatrix.D1, offsetMatrix.D2, offsetMatrix.D3, offsetMatrix.D4
);

// Adjust translation by scale
bindPose.Translation *= (float)scale;
//var translate = Float4x4.CreateScale((float)scale);
//bindPose = Maths.Mul(translate, bindPose);

mesh.bindPoses[i] = bindPose;

Expand Down Expand Up @@ -448,83 +434,87 @@ private static void LoadAnimations(Assimp.Scene? scene, double scale, List<Anima
animation.Duration = anim.DurationInTicks / (anim.TicksPerSecond != 0 ? anim.TicksPerSecond : 25.0);
animation.TicksPerSecond = anim.TicksPerSecond;
animation.DurationInTicks = anim.DurationInTicks;

foreach (var channel in anim.NodeAnimationChannels)
{
Assimp.Node boneNode = scene.RootNode.FindNode(channel.NodeName);

var animBone = new AnimationClip.AnimBone();
animBone.BoneName = boneNode.Name;

// construct full path from RootNode to this bone
// RootNode -> 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);
}
Expand Down
2 changes: 1 addition & 1 deletion Prowl.Runtime/Components/ModelRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
94 changes: 92 additions & 2 deletions Prowl.Runtime/Resources/AnimationClip.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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));
}

/// <summary>
/// Normalize a quaternion
/// </summary>
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
);
}
}
}
3 changes: 3 additions & 0 deletions Prowl.Runtime/Resources/Model.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ public class Model : EngineObject
public List<AnimationClip> 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;
Expand Down
Loading